2

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 ?

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • Both approaches are not equivalent because you use either the tzdb-rules embedded in Threeten-BP or you use the platform zone rules which might be different due to different underlying tzdb versions. – Meno Hochschild Apr 11 '18 at 14:47
  • @MenoHochschild Since the library is supposed to port Java API, it should be the same as on the framework – android developer Apr 11 '18 at 14:49
  • Oh no, if you use `Calendar.getInstance()` then you use the tz-data of Android platform (`java.util.TimeZone`) not those of ThreetenABP (`ZoneId`). – Meno Hochschild Apr 11 '18 at 15:03
  • And using `TimeZone.getDefault().getRawOffset()` does not take the whole offset. Why do you not call `getOffset(...)`? – Meno Hochschild Apr 11 '18 at 15:27
  • @MenoHochschild If you have a solution that's better than the accepted answer, please post about it. It's hard to understand what you mean here. – android developer Apr 12 '18 at 06:40

2 Answers2

5

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();
carlBjqsd
  • 130
  • 4
0

The answer of carlBjqsd is okay, just awkward and should maybe a little bit clearer.

Why one hour difference

See the final solution of @carlBjqsd: It uses the expression

int offset = zone.getOffset(1, now.getYear(), now.getMonthValue() - 1, now.getDayOfMonth(), dayOfWeek, now.getNano() / 1000000);

instead of

getRawOffset().

That has caused the difference of one hour you observed. Applications have normally no need only to calculate with the raw offset which leaves out the dst-offset for some periods of the year. It is only the total offset which matters in any conversion from local timestamp to UTC and back. The main purpose of the fine-granular differentiation of partial offsets like raw offsets or dst offsets is just proper naming of the zone (shall we call it standard time or not?).

Misleading title of question: "without loading zones"

No, you can never avoid loading zones if you want to convert between local timestamps and UTC using zones. Your real question is rather: How to avoid loading the zones of ThreetenABP and to use/load the zones of the Android platform instead. And your motivation seems to be:

I'm trying to avoid initialization of the library, which loads quite a large file of zones into it

Well, I have not measured which zone data have more impact on performance. I can only say based on my studies and knowledge of the source code of involved libraries that java.time and ThreetenBP load the whole file TZDB.dat into a binary array cache in memory (as first step) and then pick out the relevant part for a single zone (i.e. interprete a part of the binary data array via deserialization into a set of zone rules and finally a single ZoneId). Old Java platforms instead work with a set of different zi-files (one for each zone), and I suspect that Android zones behave in a similar way (but please correct me if you know that detail better).

If only ONE zone shall be used at all then the traditional approach of using separate zone files might be better but once you want to iterate over all available zones then it is better to have only one zone file at all.

Personally, I think that the performance aspect is neglectable. If you use the Android zones you will also have some loading times, inevitably. In case you really want to speed up the initialization time of ThreetenABP, you should consider to load it in a background thread.

Are Android zones and ThreetenABP zones equivalent?

Generally not. Both timezone repositories might give the same offset for a concrete zone. And often they do so but sometimes there will be differences which are not under your control. Although both timezone repositories use the data of iana.org/tz in final consequence, differences are mainly caused by possible different versions of tzdb-data. And you cannot control which version of zone data exists on the Android platform because this is up to the user of mobile phone how often he/she updates the Android OS. And this is also true for the data of ThreetenABP. You can offer the latest version of your app including the latest version of ThreetenABP but you cannot control if the mobile device user really updates the app.

Other reasons why to care about choosing the proper tz repository?

Beyond performance and initialization times, there is indeed one special scenario which might be interesting for the choice. If the Android OS is somehow old and uses an outdated version of zone rules then some mobile phone users do not update their operating system but manipulate the device clock in order to compensate the wrong timezone data. This way, they still get the correct local times on the mobile phone (in ALL apps).

In this scenario, ThreetenABP does not offer a good solution because combining their correct zone data with wrong device clock will result in wrong local timestamps (annoying the user). This has been a problem for example in Turkey which changed the dst-rules not a long time ago.

Using just the old calendar and timezone API of Android (in the package java.util) can take into account the problem so correct local timestamps are created. However, if an app communicates UTC-times (for example as count of millisecs since 1970-01-01T00Z) to other hosts (for example servers) then the wrong device clock is still a problem.

We could say why bother because the user has done "nonsense" with the device configuration but we also live in real world and should think about how to make even such users happy. So when thinking about a solution I had introduced at least in my calendar library Time4A methods like SystemClock.inPlatformView() which uses the (probably) most actual zone data and obtains the correct UTC clock based on the assumption that the user will at least observe correct local device time (whatever he/she had done to achieve this goal, either by updating the OS or by clock/zone configuration). I am quite happy with avoiding the old calendar and zone API altogether this way. My API even allows to simultaneously use both zone repositories:

  • Timezone.of("java.util.TimeZone~Asia/Jerusalem") // uses Android data
  • Timezone.of("Asia/Jerusalem") // uses Time4A data

Maybe you can profit from these ideas when to find/develop suitable helper classes for your usage of ThreetenABP. Time4A is open source.

Meno Hochschild
  • 42,708
  • 7
  • 104
  • 126
  • How exactly can Time4A help here? Does it help avoid the initialization of the ThreetenABP library, and yet allow me to use time-zone related classes (such as ZonedDateTime) without worrying ? The problem with initialization is that it takes some time, and doing it in the background is an issue because you don't know when you'd need to use it. Could be during initialization too, and then it won't help at all with performance, because you still need to wait for it. Question is, can I safely avoid zones loading by the library, and use the ones of the OS ? – android developer Apr 17 '18 at 14:01
  • @androiddeveloper Logically, Time4A is a replacement not an extra for ThreetenABP. About zone loading, if you use ThreetenABP then you cannot avoid loading of the zone data of ThreetenABP. If you use Time4A instead then you have still the freedom if you want to use the zone data of Time4A (includes extra lazy loading) or those of Android (via access to `java.util.TimeZone`). However, I can imagine to introduce a central configuration parameter for users to always suppress the built-in zone data of Time4A (in near future, would be more convenient). – Meno Hochschild Apr 17 '18 at 14:46
  • Does Time4A offer the same classes offered like ThreetenABP? I mainly use the LocalDate, LocalDateTime, and sadly ZonedDateTime – android developer Apr 17 '18 at 15:01
  • Why not offer the same classes names and APIs as on Java, and just add functionality to them? – android developer Apr 17 '18 at 16:32
  • I didn't mean use the same package name. I meant use same classes names. Just like on ThreetenABP. This can also help for the time that the app will reach minSdk high enough to use the built in API : just replace the package names in the "import" parts of the whole project. – android developer Apr 18 '18 at 06:09
  • I'm not familiar enough with Time4J library to request it. I just think that in general, a library that helps with something that exists only from specific API should try to be as similar to it as possible, to help for the time it won't be needed. It can also help for documentation, for questions about it (which will be the same as of the official API), etc... – android developer Apr 18 '18 at 12:25
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/169246/discussion-between-meno-hochschild-and-android-developer). – Meno Hochschild Apr 18 '18 at 13:33