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