Search code examples
androidtimezoneutcthreetenbp

How to convert LocalDateTime to UTC and back, without loading zones?


Background

I'm using threetenbp backport for Android (here), to handle various time related data operations.

One of them is to convert a time to a different timezone (current to UTC and back).

I know this is possible if you use something like that:

LocalDateTime now = LocalDateTime.now();
LocalDateTime nowInUtc = now.atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC")).toLocalDateTime();

This works just fine, and it's also quite easy to do the opposite.

The problem

I'm trying to avoid initialization of the library, which loads quite a large file of zones into it. I've already figured out how to handle various date/time related operations without this, except this case of converting to UTC and back.

What I got has an error of a whole 1 hour off from the correct conversion.

What I've tried

This is what I've found and tried:

// getting the current time, using current time zone: 
Calendar cal = Calendar.getInstance();
LocalDateTime now = LocalDateTime.of(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.HOUR_OF_DAY),
            cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND), cal.get(Calendar.MILLISECOND) * 1000000);

//the conversion itself, which is wrong by 1 hour in my tests: 
LocalDateTime alternativeNowInUtc = now.atZone(ZoneOffset.ofTotalSeconds(TimeZone.getDefault().getRawOffset() / 1000)).withZoneSameInstant(ZoneId.ofOffset("UTC", ZoneOffset.ofHours(0))).toLocalDateTime();

The question

What's wrong exactly with what I wrote? How can I get an alternative code for converting the time without initialization of the library?

Given an instance of LocalDateTime as input, how can I convert it from current timezone to UTC, and from UTC to current timezone ?


Solution

  • This is probably happening because your JVM's default timezone is in Daylight Saving Time (DST).

    To get the correct offset, you should check if the timezone is in DST and add this to the offset:

    Calendar cal = Calendar.getInstance();
    
    TimeZone zone = TimeZone.getDefault();
    // if in DST, add the offset, otherwise add zero
    int dst = zone.inDaylightTime(cal.getTime()) ? zone.getDSTSavings() : 0;
    int offset = (zone.getRawOffset() + dst) / 1000;
    LocalDateTime alternativeNowInUtc = now.atZone(ZoneOffset.ofTotalSeconds(offset))
        .withZoneSameInstant(ZoneId.ofOffset("UTC", ZoneOffset.ofHours(0)))
        .toLocalDateTime();
    

    Another way to create the nowInUtc as a LocalDateTime is to create an Instant from the Calendar:

    LocalDateTime nowInUtc = Instant.ofEpochMilli(cal.getTimeInMillis())
        .atOffset(ZoneOffset.ofHours(0)).toLocalDateTime();
    

    Actually, you don't need the Calendar at all, just use Instant.now() to get the current instant:

    LocalDateTime nowInUtc = Instant.now().atOffset(ZoneOffset.ofHours(0)).toLocalDateTime();
    

    Or, even shorter, use an OffsetDateTime directly:

    LocalDateTime nowInUtc = OffsetDateTime.now(ZoneOffset.ofHours(0)).toLocalDateTime();
    

    Not sure if any of those loads timezone data, it's up to you to test.

    And I think that the constant ZoneOffset.UTC can be used instead of ZoneOffset.ofHours(0), because it won't load tz data as well (but I haven't tested it).

    Final solution

    Assuming the default timezone is in Israel (TimeZone.getDefault() is Asia/Jerusalem):

    // April 11th 2018, 3 PM (current date/time in Israel)
    LocalDateTime now = LocalDateTime.of(2018, 4, 11, 15, 0, 0);
    
    TimeZone zone = TimeZone.getDefault();
    // translate DayOfWeek values to Calendar's
    int dayOfWeek;
    switch (now.getDayOfWeek().getValue()) {
        case 7:
            dayOfWeek = 1;
            break;
        default:
            dayOfWeek = now.getDayOfWeek().getValue() + 1;
    }
    // get the offset used in the timezone, at the specified date
    int offset = zone.getOffset(1, now.getYear(), now.getMonthValue() - 1,
                                now.getDayOfMonth(), dayOfWeek, now.getNano() / 1000000);
    ZoneOffset tzOffset = ZoneOffset.ofTotalSeconds(offset / 1000);
    
    // convert to UTC
    LocalDateTime nowInUtc = now
                    // conver to timezone's offset
                    .atOffset(tzOffset)
                    // convert to UTC
                    .withOffsetSameInstant(ZoneOffset.UTC)
                    // get LocalDateTime
                    .toLocalDateTime();
    
    // convert back to timezone
    LocalDateTime localTime = nowInUtc
        // first convert to UTC
        .atOffset(ZoneOffset.UTC)
        // then convert to your timezone's offset
        .withOffsetSameInstant(tzOffset)
        // then convert to LocalDateTime
        .toLocalDateTime();