Search code examples
javadatetimetimetimezonedst

How to add time to DateTime when timezone changes?


I have the below code to add time to a DateTime instance:

        DateTime d1 = new DateTime();
        d1 = d1.withZone(DateTimeZone.forID("Europe/London"));
        ArrayList<String> timeList = new ArrayList<String>();

        for(int x = 1; x <= 10; x++) {
        //Adds six hours to the DateTime instance.
                d1 = d1.plusHours(6);
                d1 = d1.plusMinutes(0);
                d1 = d1.plusSeconds(0);
                timeList.add(d1.toString());
        }

This will create a set of 10 times to add to an arraylist. However say there was a daylight saving change when the 6 hours were added. How can I generate the correct time when the extra hour is added/removed due to the timezone change? At the moment it does not remove/add the extra hour using this method.

For example I would expect the below times to be generated if I was to begin running the code on the 24th Oct 2015 at 10:00am. Note that the timezone changes at 02:00am on 25/10/2015.

24/10/2015 10:00:00 BST
24/10/2015 16:00:00 BST
24/10/2015 22:00:00 BST
25/10/2015 05:00:00 GMT
25/10/2015 11:00:00 GMT
25/10/2015 17:00:00 GMT
25/10/2015 23:00:00 GMT
26/10/2015 05:00:00 GMT
26/10/2015 11:00:00 GMT
26/10/2015 17:00:00 GMT

Solution

  • Don’t worry, use java.time

    • Apparently you are using the Joda-Time library. Use java.time instead.
    • The java.time classes automatically handle Daylight Saving Time (DST) cutovers.
    • No need for you to do anything except keep your JVM up-to-date with tzdata time zone database changes that may affect your time zones of interest. See Oracle’s provided tool for tzdata updates.

    java.time

    Let's look at the results of adding six hours using the java.time classes.

    Define the date and the time-of-day portions.

    LocalDate ld = LocalDate.of ( 2015, Month.OCTOBER, 24 );  // 24th Oct 2015 at 10:00am per the Question.
    LocalTime lt = LocalTime.of ( 10, 0 );
    

    Define the time zone, a ZoneId object, for Europe/London.

    ZoneId z = ZoneId.of ( "Europe/London" );
    

    Combine to create a ZonedDateTime object.

    ZonedDateTime zdtStart = ZonedDateTime.of ( ld, lt, z );
    

    Extract an Instant from the ZonedDateTime. The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds (up to nine (9) digits of a decimal fraction).

    Instant instantStart = zdtStart.toInstant ( );
    

    Define our span of time, six hours, as a Duration. The java.time classes can perform date-time math by adding a Duration object.

    A Duration is unattached to the timeline, and actually stores a number of seconds and a number of nanoseconds. So there are no smarts about “six hours” and the clock and DST etc. within this class. When we ask for a Duration of six hours, that class immediately calculates ( 6 hours * 60 minutes per hour * 60 seconds per minute ) = 21,600 seconds in total.

    Duration sixHours = Duration.ofHours ( 6 );  // 21,600 seconds = ( 6 hours * 60 minutes per hour * 60 seconds per minute ).
    

    Loop ten times. First loop by adding the Duration to ZonedDateTime, and convert the result to an Instant.

    // Increment the `ZonedDateTime`.
    ZonedDateTime zdt = zdtStart;
    for ( int i = 1 ; i <= 10 ; i++ ) {
        System.out.println ( ">zdt.toString() " + zdt + " | zdt.toInstant().toString(): " + zdt.toInstant ( ) + "\n");
        // Set up next loop.
        zdt = zdt.plus ( sixHours );
    }
    

    When run. Note the jump in the time-of-day in London time. This is the Daylight Saving Time (DST) cutover, the “Fall-back” time in the autumn when England switches back to standard time going from an offset-from-UTC of +01:00 to Zulu offset of +00:00, where at 2 AM the clock jumps back to repeat the 1 AM hour. So where we would otherwise expect 22:00 plus six hours to result in 4 AM, we instead see 3 AM. You can see in the Instant value that six hours did indeed elapse. The trick was that Londoners wound-back their clocks an hour around then.

    See the history of DST cutovers for Europe/London.

    zdt.toString() 2015-10-24T10:00+01:00[Europe/London] | zdt.toInstant().toString(): 2015-10-24T09:00:00Z

    zdt.toString() 2015-10-24T16:00+01:00[Europe/London] | zdt.toInstant().toString(): 2015-10-24T15:00:00Z

    zdt.toString() 2015-10-24T22:00+01:00[Europe/London] | zdt.toInstant().toString(): 2015-10-24T21:00:00Z

    zdt.toString() 2015-10-25T03:00Z[Europe/London] | zdt.toInstant().toString(): 2015-10-25T03:00:00Z

    zdt.toString() 2015-10-25T09:00Z[Europe/London] | zdt.toInstant().toString(): 2015-10-25T09:00:00Z

    zdt.toString() 2015-10-25T15:00Z[Europe/London] | zdt.toInstant().toString(): 2015-10-25T15:00:00Z

    zdt.toString() 2015-10-25T21:00Z[Europe/London] | zdt.toInstant().toString(): 2015-10-25T21:00:00Z

    zdt.toString() 2015-10-26T03:00Z[Europe/London] | zdt.toInstant().toString(): 2015-10-26T03:00:00Z

    zdt.toString() 2015-10-26T09:00Z[Europe/London] | zdt.toInstant().toString(): 2015-10-26T09:00:00Z

    zdt.toString() 2015-10-26T15:00Z[Europe/London] | zdt.toInstant().toString(): 2015-10-26T15:00:00Z

    For fun we swap, adding the six hours successively to Instant and convert the result to London time.

    // Increment the `Instant`.
    Instant instant = instantStart;
    for ( int i = 1 ; i <= 10 ; i++ ) {
        System.out.println ( ">instant.toString() " + instant + " | instant.atZone(z).toString(): " + instant.atZone ( z ) + "\n");
        // Set up next loop.
        instant = instant.plus ( sixHours );
    }
    

    When run, we see the same values output.

    instant.toString() 2015-10-24T09:00:00Z | instant.atZone(z).toString(): 2015-10-24T10:00+01:00[Europe/London]

    instant.toString() 2015-10-24T15:00:00Z | instant.atZone(z).toString(): 2015-10-24T16:00+01:00[Europe/London]

    instant.toString() 2015-10-24T21:00:00Z | instant.atZone(z).toString(): 2015-10-24T22:00+01:00[Europe/London]

    instant.toString() 2015-10-25T03:00:00Z | instant.atZone(z).toString(): 2015-10-25T03:00Z[Europe/London]

    instant.toString() 2015-10-25T09:00:00Z | instant.atZone(z).toString(): 2015-10-25T09:00Z[Europe/London]

    instant.toString() 2015-10-25T15:00:00Z | instant.atZone(z).toString(): 2015-10-25T15:00Z[Europe/London]

    instant.toString() 2015-10-25T21:00:00Z | instant.atZone(z).toString(): 2015-10-25T21:00Z[Europe/London]

    instant.toString() 2015-10-26T03:00:00Z | instant.atZone(z).toString(): 2015-10-26T03:00Z[Europe/London]

    instant.toString() 2015-10-26T09:00:00Z | instant.atZone(z).toString(): 2015-10-26T09:00Z[Europe/London]

    instant.toString() 2015-10-26T15:00:00Z | instant.atZone(z).toString(): 2015-10-26T15:00Z[Europe/London]

    See this code run live at IdeOne.com.


    About java.time

    The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

    The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

    To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

    Where to obtain the java.time classes?

    The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.