Pacmyc
Pacmyc

Reputation: 49

Converting UTC to Local Time with daylight saving in Java

I am trying to convert UTC time to local time, including daylight saving. Localtime (Stockholm), in summer, should be 2 hours ahead of UTC, but when I convert it in Java it only adds one hour.

public class TimeConverter {

    public static void main(String[] args) throws ParseException {
        getLocalTime("2:36:10 AM");
    }

    public static String getLocalTime(String utcTime) throws ParseException {

        DateFormat utc = new SimpleDateFormat("hh:mm:ss a");
        utc.setTimeZone(TimeZone.getTimeZone("UTC"));

        Date date = utc.parse(utcTime);

        DateFormat local = new SimpleDateFormat("HH:mm:ss");
        local.setTimeZone(TimeZone.getDefault());

        System.out.println(utc.getTimeZone());
        System.out.println(local.getTimeZone());

        System.out.println("UTC TIME\t" + utcTime);
        System.out.println("LOCAL TIME\t" + local.format(date));

        return local.format(date);
    }
}

Output:

sun.util.calendar.ZoneInfo[id="UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
sun.util.calendar.ZoneInfo[id="Europe/Stockholm",offset=3600000,dstSavings=3600000,useDaylight=true,transitions=143,lastRule=java.util.SimpleTimeZone[id=Europe/Stockholm,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]]
UTC TIME    2:36:10 AM
LOCAL TIME  03:36:10

Upvotes: 0

Views: 1451

Answers (4)

Basil Bourque
Basil Bourque

Reputation: 338564

tl;dr

Your code inadvertently used the moment of 1970-01-01T02:36:10Z, 2 AM on the first day of 1970 as seen in UTC. Then you adjusted to Stockholm time, still in 1970 when no DST was in effect. Thus an offset of one hour (+01:00) rather than the two hours (+02:00) that you expected.

Use modern java.time classes instead.

OffsetDateTime                                     // Represent a date, a time-of-day, and an offset-from-UTC.
        .of(                                       // Static factory method.
                LocalDate.now( ZoneOffset.UTC ) ,  // Date
                LocalTime.parse( "02:36:10" ) ,    // Time-of-day
                ZoneOffset.UTC                     // A constant representing an offset from UTC of zero hours-minutes-seconds.
        )                                          // Returns an `OffsetDateTime` object.
        .atZoneSameInstant(                        // Adjust to a specific time zone. Same moment, different wall-clock time/calendar.
                ZoneId.of( "Europe/Stockholm" )
        )                                          // Returns a `ZonedDateTime` object.
        .toString()                                // Returns a `String` object containing text in standard ISO 8601 format wisely extended by appending the name of the time zone in square brackets.

2023-05-06T04:36:10+02:00[Europe/Stockholm]

We see your 2 hour offset, as expected.

Avoid legacy date-time classes

You are using terrible date-time classes that are now legacy, supplanted years ago by the modern java.time classes defined in JSR 310.

Time-of-day in UTC makes no sense

Asking about a time of day alone in UTC makes no sense. You need a date as well as a time to determine a moment, to represent a point on the timeline.

When communicating data textually to represent a time, use standard ISO 8601 format. For a time of day that means simply 24-hour clock with padding zeros.

LocalTime lt = LocalTime.parse( "02:36:10" ) ;

LocalDate & OffsetDateTime

Apply a date to determine a moment. Perhaps you intended the current date as seen in UTC.

LocalDate todayUtc = LocalDate.now( ZoneOffset.UTC ) ;
OffsetDateTime odt = OffsetDateTime.of( todayUtc , lt , ZoneOffset.UTC ) ;

ZonedDateTime

Adjust to a time zone. Same moment, different wall-clock time & calendar.

ZoneId zStockholm = ZoneId.of( "Europe/Stockholm" ) ;
ZonedDateTime zdtStockholm = odt.atZoneSameInstant( zStockholm ) ;

Generate text in standard ISO 8601 format.

String output = zdtStockholm.toString() ;

2023-05-06T04:36:10+02:00[Europe/Stockholm]

There we see the two-hour offset you expected.

Your code uses 1970

Your code failed because your java.util.Date object represents 2 AM on the first day of 1970 as seen in UTC.

Avoid calling java.util.Date#toString. That method unfortunately applies the JVM‘s current default time zone while generating its text. That poor design decision muddies the water.

For convenience, we convert your legacy java.util.Date object to its modern replacement, a java.time.Instant object — both represent a moment as seen with an offset from UTC of zero hours-minutes-seconds. Fortunately, Instant#toString tells the truth, unlike Date#toString.

System.out.println( new SimpleDateFormat( "hh:mm:ss a" ).parse( "2:36:10 AM" ).toInstant() );

1970-01-01T10:36:10Z

Let's look at the time zone rules for Sweden for that moment.

Instant instant = new SimpleDateFormat( "hh:mm:ss a" ).parse( "2:36:10 AM" ).toInstant(); 
ZoneRules rules = ZoneId.of( "Europe/Stockholm" ).getRules();
boolean isDst = rules.isDaylightSavings( instant );
System.out.println( "isDst = " + isDst );

isDst = false

So there was 👉 no Daylight Saving Time (DST) in effect in that Sweden time zone at that moment in 1970.

rules.getTransitions().toString()

We can see in that list of transitions that between 1949-10-02 and 1980-04-06 the offset was +01:00. So no +02:00 as you expected.

Upvotes: 6

deHaar
deHaar

Reputation: 18568

java.time is a better choice than older packages/classes.

See this example, which takes today as date and converts from UTC to Europe/Stockholm:

public static void main(String[] args) {
    // input time as String
    String someTime = "2:36:10 AM";
    // create a formatter for those time Strings
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("h:m:s a", Locale.ENGLISH);
    // parse the time (time only, no date or zone involved so far)
    LocalTime localTime = LocalTime.parse(someTime, dtf);
    // involve a date (today here)
    LocalDateTime localDateTime = LocalDateTime.of(LocalDate.now(), localTime);
    // create a zone to involve
    ZoneId stockholm = ZoneId.of("Europe/Stockholm");
    // compose the (previously composed) date and time, and the zone
    ZonedDateTime utcTime = ZonedDateTime.of(localDateTime, ZoneOffset.UTC);
    // then use that moment in time and express it in a different zone (UTC here)
    ZonedDateTime stockholmTime = utcTime.withZoneSameInstant(stockholm);
    // print both zoned datetimes
    System.out.println("UTC time:   " + utcTime);
    System.out.println("local time: " + stockholmTime);
}

Output:

UTC time:   2023-05-07T02:36:10Z
local time: 2023-05-07T04:36:10+02:00[Europe/Stockholm]

Upvotes: 1

Samuel Marchant
Samuel Marchant

Reputation: 320

Maybe you have not updated the IANNA time zones database for your Java version, read this go to the index on the page for java time.(html page) https://drive.google.com/file/d/1gjHmdC-BW0Q2vXiQYmp1rzPU497sybNy/view?usp=drivesdk

And that's also a truth Calendar and TimeZone in java.util are thereabout completely deprecated since Sun/Oracle Java 1.8.0 Calendar TimeZones Offset has two different offsets "Raw" (geographic official iso) and actual, and are set in seconds +/- .

Second, Calendar for most European purpose is actually the Gregorian sub class instance, Calendar itself is simply a top level abstraction class.

Upvotes: -3

Jim Garrison
Jim Garrison

Reputation: 86774

This answers the OP's question (why is this happening), as others have already recommended to use the modern date/time classes.

The definition of SimpleDateFormat#parse(...) includes:

public Date parse​(String text,
   ParsePosition pos)

Parses text from a string to produce a Date.

The method attempts to parse text starting at the index given by pos.

...

This parsing operation uses the calendar to produce a Date. All of the calendar's date-time fields are cleared before parsing, and the calendar's default values of the date-time fields are used for any missing date-time information. For example, the year value of the parsed Date is 1970 with GregorianCalendar if no year value is given from the parsing operation. The TimeZone value may be overwritten, depending on the given pattern and the time zone value in text. Any TimeZone value that has previously been set by a call to setTimeZone may need to be restored for further operations.

In 1970 Stockholm did not observer DST and was therefore +01:00

Upvotes: 1

Related Questions