I am processing a file line by line.
Each line has a date in the following format: YYMMDD HHMM
The file is basically recording a reading every 15 minutes. The recording is using day light savings for the time zone it is in. The issue I am having is during spring ahead and fall back. The recording is duplicating dates when fall back occurs and a gap when fall back occurs.
Fallback example:
141102 0100
141102 0115
141102 0130
141102 0145
141102 0200
141102 0115 - duplicate
141102 0130 - duplicate
141102 0145 - duplicate
141102 0200 - duplicate
Spring forward example:
150308 0200
150308 0315
What I need to be able to do, is shift all the dates forward an hour when in daylight savings time.
The program I am working on is using java.util.Calendar currently. I have tried doing the shift using the java Calendar and am running into lots of issues with adding the dates. I assume the problem to be that it is trying to correct DST issues itself, when all I want to do is shift the date by an hour. It also has an issue with detecting that gap. For example it thinks that the first 1:15 - 2:00 is in daylight time, when this is not the case for it.
Basically what I want is to change the fallback example to everything that falls in this date range to shift back an hour:
Fallback example:
141102 0100 no change
141102 0115 no change
141102 0130 no change
141102 0145 no change
141102 0200 no change
141102 0115 to 141102 0215
141102 0130 to 141102 0230
141102 0145 to 141102 0245
141102 0200 to 141102 0300
Keeps on changing date until it hits the spring forward
150308 0200 to 150308 0300
150308 0315 no change.
I have tried lots of ways, and can't seem to come to a solution. I am not opposed to using Joda Time as I've been looking into that a bit as well. Any help would be appreciated, thanks.
UTC
Always use UTC for data exchange and this kind of readings log. Always, always, always use UTC. Think of UTC as the One True Time and other time zones as deviations from UTC.
By the way, Daylight Saving Time (DST) is not the only problem to worry about with zoned date-time values. Other anomalies occur in various time zones resulting in wall-clock time shifting forward or backward. The solution is easy: Avoid all time zones – Use UTC.
Instant
The Instant
class captures a moment on the timeline in UTC with a resolution of nanoseconds. This should be you go-to class for date-time work.
Instant instant = Instant.now();
Strings
For logging, generate a string in standard ISO 8601 format. Stick with the proven ISO 8601 formats rather than invent your own.
The java.time classes including Instant
use ISO 8601 formats by default when parsing/generating strings. So no need to specify formatting patterns.
String output = instant.toString();
2016-11-02T21:10:05.321Z
Instant instant = Instant.parse( "2016-11-02T21:10:05.321Z" );
And always include the century 20
in your dates. Omitting just creates so much opportunity for errors in misinterpreting. Storage and memory really is cheap enough that we can afford the two extra digits.
Time zone indicator
Always include a time zone or offset-from-UTC indicator with your serialized date-time values.
In the Instant::toString
result seen above, the Z
stands for Zulu
which means UTC.
Parsing strings
Here is code to parse your date-time strings. We parse as LocalDateTime
first, without any time zone or offset, because your input lacks any indicator of time zone or offset. Then we assign a time zone to get a ZonedDateTime
, to see how it handles the moment of DST cut-over.
I assume your time zone in question uses a DST fall-back cutover of 2 AM. I used America/Montreal
as an example of such.
List<String> inputs = new ArrayList<>( 2 );
inputs.add( "141102 0115" ); // 1 AM
inputs.add( "141102 0215" ); // 2 AM
for( String input : inputs ) {
DateTimeFormatter f = DateTimeFormatter.ofPattern( "uuMMdd HHmm" );
LocalDateTime ldt = LocalDateTime.parse( input , f );
// At 2 AM in this zone, the clock falls back one hour for DST cutover. The 1 AM hour repeats.
ZoneId z = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ldt.atZone( z );
System.out.println( "input: " + input );
System.out.println( "ldt: " + ldt );
System.out.println( "zdt: " + zdt );
System.out.println( "instant: " + zdt.toInstant() );
System.out.println("");
}
input: 141102 0115
ldt: 2014-11-02T01:15
zdt: 2014-11-02T01:15-04:00[America/Montreal]
instant: 2014-11-02T05:15:00Z
…and…
input: 141102 0215
ldt: 2014-11-02T02:15
zdt: 2014-11-02T02:15-05:00[America/Montreal]
instant: 2014-11-02T07:15:00Z
You can see this code live in IdeOne.com. Note the two hour difference in UTC values (Z
).
The behavior we see is documented.
In most cases, there is only one valid offset for a local date-time. In the case of an overlap, where clocks are set back, there are two valid offsets. This method uses the earlier offset typically corresponding to "summer".
Where the 1:15 AM is ambiguous, could be either the first or second occurrence, java.time goes with the first.
You could play a guessing-game to try to adjust these poorly-designed strings. If you are sure you have at least one sample during every clock hour, you could keep track of previous samples and see if the sample being processed has a clock-time earlier than the previous. If so you could assume this is a DST fall-back cutover, and add an hour with plusHours( 1 )
.
if( processingZdt.toLocalDateTime().isBefore( previousZdt.toLocalDateTime() ) {
processingZdt.plusHours( 1 ) ; // A hack; I am *not* recommending this.
…
But this is a risky hack of a solution. I'm not sure this work, as I've not thought it through. If desperate, perhaps you can make it work.
The much wise approach is prevention: Use UTC.