Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

Using Java I have a SimpleTimeZone instance with GMT offset and daylight saving time information from a legacy system.

I would like to retrieve ZoneId to be able to use Java 8 time API.

Actually, toZoneId returns a ZoneId without the daylight Saving time infos

SimpleTimeZone stz = new SimpleTimeZone( 2 * 60 * 60 * 1000, "GMT", Calendar.JANUARY,1,1,1, Calendar.FEBRUARY,1,1,1, 1 * 60 * 60 * 1000);
stz.toZoneId();
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
165 views
Welcome To Ask or Share your Answers For Others

1 Answer

First of all, when you do:

SimpleTimeZone stz = new SimpleTimeZone(2 * 60 * 60 * 1000, "GMT", Calendar.JANUARY, 1, 1, 1, Calendar.FEBRUARY, 1, 1, 1, 1 * 60 * 60 * 1000);

You're creating a timezone with ID equals to "GMT". When you call toZoneId(), it just calls ZoneId.of("GMT") (it uses the same ID as parameter, as already told in @Ole V.V.'s answer). And then ZoneId class loads whatever Daylight Saving infos are configured in the JVM (it doesn't keep the same rules from the original SimpleTimeZone object).

And according to ZoneId javadoc: If the zone ID equals 'GMT', 'UTC' or 'UT' then the result is a ZoneId with the same ID and rules equivalent to ZoneOffset.UTC. And ZoneOffset.UTC has no DST rules at all.

So, if you want to have a ZoneId instance with the same DST rules, you'll have to create them by hand (I didn't know it was possible, but it actually is, check below).


Your DST rules

Looking at SimpleTimeZone javadoc, the instance you created has the following rules (according to my tests):

  • standard offset is +02:00 (2 hours ahead UTC/GMT)
  • DST starts at the first Sunday of January (take a look at the javadoc for more details), 1 millisecond after midnight (you passed 1 as start and end times)
  • when in DST, offset changes to +03:00
  • DST ends at the first Sunday of February, 1 millisecond after midnight (then offset gets back to +02:00)

Actually, according to javadoc, you should've passed a negative number in dayOfWeek parameters to work this way, so the timezone should be created like this:

stz = new SimpleTimeZone(2 * 60 * 60 * 1000, "GMT", Calendar.JANUARY, 1, -Calendar.SUNDAY, 1, Calendar.FEBRUARY, 1, -Calendar.SUNDAY, 1, 1 * 60 * 60 * 1000);

But in my tests, both worked the same way (maybe it fixes the non-negative values). Anyway, I've made some tests just to check these rules. First I created a SimpleDateFormat with your custom timezone:

TimeZone t = TimeZone.getTimeZone("America/Sao_Paulo");
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss Z");
sdf.setTimeZone(t);

Then I tested with the boundaries dates (before start and end of DST):

// starts at 01/01/2017 (first Sunday of January)
ZonedDateTime z = ZonedDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.ofHours(2));
// 01/01/2017 00:00:00 +0200 (not in DST yet, offset is still +02)
System.out.println(sdf.format(new Date(z.toInstant().toEpochMilli())));
// 01/01/2017 01:01:00 +0300 (DST starts, offset changed to +03)
System.out.println(sdf.format(new Date(z.plusMinutes(1).toInstant().toEpochMilli())));

// ends at 05/02/2017 (first Sunday of February)
z = ZonedDateTime.of(2017, 2, 5, 0, 0, 0, 0, ZoneOffset.ofHours(3));
// 05/02/2017 00:00:00 +0300 (in DST yet, offset is still +03)
System.out.println(sdf.format(new Date(z.toInstant().toEpochMilli())));
// 04/02/2017 23:01:00 +0200 (DST ends, offset changed to +02 - clock moves back 1 hour: from midnight to 11 PM of previous day)
System.out.println(sdf.format(new Date(z.plusMinutes(1).toInstant().toEpochMilli())));

The output is:

01/01/2017 00:00:00 +0200
01/01/2017 01:01:00 +0300
05/02/2017 00:00:00 +0300
04/02/2017 23:01:00 +0200

So, it follows the rules described above (at 01/01/2017 midnight the offset is +0200, one minute later it's in DST (offset is now +0300; the opposite occurs at 05/02 (DST ends and offset goes back to +0200)).


Create a ZoneId with the rules above

Unfortunatelly you can't extend ZoneId and ZoneOffset, and you can't also change them because both are immutable. But it's possible to create custom rules and assign them to a new ZoneId.

And it doesn't seem to have a way to directly export the rules from SimpleTimeZone to ZoneId, so you'll have to create them by hand.

First we need to create a ZoneRules, a class that contains all the rules to when and how the offset changes. In order to create it, we need to build a list of 2 classes:

  • ZoneOffsetTransition: defines a specific date for an offset change. There must be at least one to make it work (with an empty list it failed)
  • ZoneOffsetTransitionRule: defines a general rule, not bounded to a specific date (like "first Sunday of January the offset changes from X to Y"). We must have 2 rules (one for DST start, and another for DST end)

So, let's create them:

// offsets (standard and DST)
ZoneOffset standardOffset = ZoneOffset.ofHours(2);
ZoneOffset dstOffset = ZoneOffset.ofHours(3);

// you need to create at least one transition (using a date in the very past to not interfere with the transition rules)
LocalDateTime startDate = LocalDateTime.MIN;
LocalDateTime endDate = LocalDateTime.MIN.plusDays(1);
// DST transitions (date when it happens, offset before and offset after) - you need to create at least one
ZoneOffsetTransition start = ZoneOffsetTransition.of(startDate, standardOffset, dstOffset);
ZoneOffsetTransition end = ZoneOffsetTransition.of(endDate, dstOffset, standardOffset);
// create list of transitions (to be used in ZoneRules creation)
List<ZoneOffsetTransition> transitions = Arrays.asList(start, end);

// a time to represent the first millisecond after midnight
LocalTime firstMillisecond = LocalTime.of(0, 0, 0, 1000000);
// DST start rule: first Sunday of January, 1 millisecond after midnight
ZoneOffsetTransitionRule startRule = ZoneOffsetTransitionRule.of(Month.JANUARY, 1, DayOfWeek.SUNDAY, firstMillisecond, false, TimeDefinition.WALL,
    standardOffset, standardOffset, dstOffset);
// DST end rule: first Sunday of February, 1 millisecond after midnight
ZoneOffsetTransitionRule endRule = ZoneOffsetTransitionRule.of(Month.FEBRUARY, 1, DayOfWeek.SUNDAY, firstMillisecond, false, TimeDefinition.WALL,
    standardOffset, dstOffset, standardOffset);
// list of transition rules
List<ZoneOffsetTransitionRule> transitionRules = Arrays.asList(startRule, endRule);

// create the ZoneRules instance (it'll be set on the timezone)
ZoneRules rules = ZoneRules.of(start.getOffsetAfter(), end.getOffsetAfter(), transitions, transitions, transitionRules);

I couldn't create a ZoneOffsetTransition that starts at the first millisecond after midnight (they actually start at midnight exactly), because the fraction of seconds must be zero (if it's not, ZoneOffsetTransition.of() throws an exception). So, I decided to set a date in the past (LocalDateTime.MIN) to not interfere with the rules.

But the ZoneOffsetTransitionRule instances works exactly like expected (DST starts and ends 1 millisecond after midnight, just like the SimpleTimeZone instance).

Now we must set this ZoneRules to a timezone. As I said, ZoneId can't be extended (the constructor is not public) and neither does ZoneOffset (it's a final class). I initially thought that the only way to set the rules was to create an instance and set it using reflection, but actually the API provides a way to create custom ZoneId's by extending the java.time.zone.ZoneRulesProvider class:

// new provider for my custom zone id's
public class CustomZoneRulesProvider extends ZoneRulesProvider {

    @Override
    protected Set<String> provideZoneIds() {
        // returns only one ID
        return Collections.singleton("MyNewTimezone");
    }

    @Override
    protected ZoneRules provideRules(String zoneId, boolean forCaching) {
        // returns the ZoneRules for the custom timezone
        if ("MyNewTimezone".equals(zoneId)) {
            ZoneRules rules = // create the ZoneRules as above
            return rules;
        }
        return null;
    }

    // returns a map with the ZoneRules, check javadoc for more details
    @Override
    protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) {
        TreeMap<String, ZoneRules> map = new TreeMap<>();
        ZoneRules rules = getRules(zoneId, false);
        if (rules != null) {
            map.put(zoneId, rules);
        }
        return map;
    }
}

Please keep in mind that you shouldn't set the ID to "GMT", "UTC", or any valid ID (you can check all existent IDs with ZoneId.getAvailableZoneIds()). "GMT" and "UTC" are special names used internally by the API and it can lead to unexpected behaviour. So choose a name that doesn't exist - I've chosen MyNewTimezone (without spaces otherwise it'll fail because ZoneRegion throws an exception if there's a space in the name).

Let's test this new timezone. The new class must be registered using the ZoneRulesProvider.registerProvider method:

// register the new zonerules provider
ZoneRulesProvider.registerProvider(new CustomZoneRulesProvider());
// create my custom zone
ZoneId customZone = ZoneId.of("MyNewTimezone");

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss Z");
// starts at 01/01/2017 (first Sunday of January)
ZonedDateTime z = ZonedDateTime.of(2017, 1, 1, 0, 0, 0, 0, customZone);
// 01/01/2017 00:00:00 +0200 (not in DST yet, offset is still +02)
System.out.println(z.format(fmt));
// 01/01/2017 01:01:00 +0300 (DST starts, offset changed to +03)
System.out.println(z.plusMinutes(1).format(fmt));

// ends at 05/02/2017 (first Sunday of February)
z = ZonedDateTime.of(2017, 2, 5, 0, 0, 0, 0, customZone);
// 05/02/2017 00:00:00 +0300 (in DST yet, offset is still +03)
System.out.println(z.format(fmt));
// 04/02/2017 23:01:00 +0200 (DST ends, offset changed to +02 - clock moves back 1 hour: from midnight to 11 PM of previous day)
System.out.println(z.plusMinutes(1).format(fmt));

The output is the same (so the rules are the same used by the SimpleTimeZone):

01/01/2017 00:00:00 +0200
01/01/2017 01:01:00 +0300
05/02/2017 00:00:00 +0300
04/02/2017 23:01:00 +0200


Notes:

  • The CustomZoneRulesProvider creates just one new ZoneId, but of course you can extend it to create more. Check the javadoc for more details about how to correct implement your own rules provider.
  • You must check exactly what are the rules from your custom timezones before creating the ZoneRules. One way is to use SimpleTimeZone.toString() method (that returns the internal state of the object) and read the javadoc to know how the paramete

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...