从SimpleTimeZone获取ZoneId
使用Java我有一个带有GMT偏移量的SimpleTimeZone
实例和来自遗留系统的夏令时信息。
我想检索ZoneId
以便能够使用Java 8时间API。
实际上, toZoneId
返回没有夏令时信息的ZoneId
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();
首先,当你这样做时:
SimpleTimeZone stz = new SimpleTimeZone(2 * 60 * 60 * 1000, "GMT", Calendar.JANUARY, 1, 1, 1, Calendar.FEBRUARY, 1, 1, 1, 1 * 60 * 60 * 1000);
您正在创建ID等于"GMT"
的时区。 当你调用toZoneId()
,它只调用ZoneId.of("GMT")
(它使用与参数相同的ID,如@Ole VV的回答中所述 )。 然后, ZoneId
类加载在JVM中配置的任何夏令时信息(它不保留与原始SimpleTimeZone
对象相同的规则)。
根据ZoneId
javadoc : 如果区域ID等于‘GMT’ ,’UTC’或’UT’,则结果是具有相同ID的ZoneId,并且规则等同于ZoneOffset.UTC 。 并且ZoneOffset.UTC
根本没有DST规则。
因此,如果你想拥有一个具有相同DST规则的ZoneId
实例,你必须手工创建它们(我不知道它是可能的,但实际上是,请查看下面的内容)。
您的DST规则
查看SimpleTimeZone
javadoc ,您创建的实例具有以下规则(根据我的测试):
- 标准偏差为
+02:00
(提前2小时UTC / GMT) - DST从1月的第一个星期日开始(请查看javadoc以获取更多详细信息), 午夜后1毫秒 (您将
1
作为开始和结束时间) - 在DST时,偏移变为
+03:00
- DST在2月的第一个星期日结束, 午夜后1毫秒 (然后偏移回到
+02:00
)
实际上,根据javadoc,你应该在dayOfWeek参数中传递一个负数以这种方式工作,所以应该像这样创建时区:
stz = new SimpleTimeZone(2 * 60 * 60 * 1000, "GMT", Calendar.JANUARY, 1, -Calendar.SUNDAY, 1, Calendar.FEBRUARY, 1, -Calendar.SUNDAY, 1, 1 * 60 * 60 * 1000);
但在我的测试中,两者都以相同的方式工作(也许它修复了非负值)。 无论如何,我做了一些测试只是为了检查这些规则。 首先,我使用您的自定义时区创建了一个SimpleDateFormat
:
TimeZone t = TimeZone.getTimeZone("America/Sao_Paulo"); SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss Z"); sdf.setTimeZone(t);
然后我测试了边界日期(在夏令时开始和结束之前):
// 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())));
输出是:
01/01/2017 00:00:00 +0200
01/01/2017 01:01:00 +0300
2017/02/05 00:00:00 +0300
2017/02/04 23:01:00 +0200
因此,它遵循上述规则(在01/01/2017午夜偏移为+0200
,一分钟后它在DST(偏移现在是+0300
;相反发生在05/02)(DST结束并且偏移返回到+0200
))。
使用上述规则创建ZoneId
不幸的是,你不能扩展ZoneId
和ZoneOffset
,你也不能改变它们,因为它们都是不可变的。 但是可以创建自定义规则并将它们分配给新的ZoneId
。
它似乎没有办法直接将规则从SimpleTimeZone
导出到ZoneId
,因此您必须手动创建它们。
首先,我们需要创建一个ZoneRules
,这个类包含偏移何时以及如何变化的所有规则。 为了创建它,我们需要构建一个包含2个类的列表:
-
ZoneOffsetTransition
:定义偏移量更改的特定日期。 必须至少有一个使它工作(空列表失败) -
ZoneOffsetTransitionRule
:定义一般规则,ZoneOffsetTransitionRule
于特定日期(例如“偏移从X变为Y的1月的第一个星期日” )。 我们必须有2个规则(一个用于DST开始,另一个用于DST结束)
那么,让我们创建它们:
// 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 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 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);
我无法创建一个在午夜后的第一个毫秒开始的ZoneOffsetTransition
(它们实际上恰好在午夜开始),因为秒的分数必须为零(如果不是,则ZoneOffsetTransition.of()
抛出exception)。 因此,我决定在过去设置一个日期( LocalDateTime.MIN
)以不干扰规则。
但是ZoneOffsetTransitionRule
实例的工作方式与预期完全相同(DST在午夜后开始和结束1毫秒,就像SimpleTimeZone
实例一样)。
现在我们必须将此ZoneRules
设置ZoneRules
区。 正如我所说, ZoneId
无法扩展(构造函数不公开), ZoneOffset
也不是( final
类)。 我最初认为设置规则的唯一方法是创建一个实例并使用reflection设置它,但实际上API提供了一种通过扩展java.time.zone.ZoneRulesProvider
类来创建自定义ZoneId
的方法 :
// new provider for my custom zone id's public class CustomZoneRulesProvider extends ZoneRulesProvider { @Override protected Set 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 provideVersions(String zoneId) { TreeMap map = new TreeMap<>(); ZoneRules rules = getRules(zoneId, false); if (rules != null) { map.put(zoneId, rules); } return map; } }
请记住,您不应将ID设置为“GMT”,“UTC”或任何有效ID(您可以使用ZoneId.getAvailableZoneIds()
检查所有现有ID)。 “GMT”和“UTC”是API内部使用的特殊名称,可能会导致意外行为。 所以选择一个不存在的名称 – 我选择了MyNewTimezone
(没有空格,否则它会失败,因为如果名称中有空格, ZoneRegion
会抛出exception)。
让我们来测试这个新的时区。 必须使用ZoneRulesProvider.registerProvider
方法注册新类:
// 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));
输出是相同的(因此规则与SimpleTimeZone
使用的相同):
01/01/2017 00:00:00 +0200
01/01/2017 01:01:00 +0300
2017/02/05 00:00:00 +0300
2017/02/04 23:01:00 +0200
笔记:
-
CustomZoneRulesProvider
只创建一个新的ZoneId
,但当然您可以扩展它以创建更多。 检查javadoc以获取有关如何更正实现自己的规则提供程序的更多详细信息。 - 在创建
ZoneRules
之前,您必须确切地检查自定义时区的规则。 一种方法是使用SimpleTimeZone.toString()
方法(返回对象的内部状态)并读取javadoc以了解参数如何影响规则。 - 我没有测试足够的案例来了解
SimpleTimeZone
和ZoneId
规则是否有某种特定日期以不同的方式表现。 我已经测试了不同年份的一些日期,似乎工作正常。
我不认为这是可能的。 人们也可能会问它会有什么意义。 如果时区不在Olson数据库中,那么它在现实世界中很难使用,那么它在你的程序中有什么用处呢? 当然,我理解你的遗留程序创建一个SimpleTimeZone
, 它确实表示在野外使用的时区,但只是给它一个不正确的ID,以便TimeZone.toZoneId()
无法进行正确的翻译。
我检查了TimeZone.toZoneId()
的源代码。 它仅依赖于从getID
方法获得的值。 它不会考虑偏移或区域规则。 而且重要的是, SimpleTimeZone
不会覆盖该方法。 因此,如果您的SimpleTimeZone
具有已知ID(包括像EST或ACT这样的皱眉),您将获得正确的ZoneId
。 否则你不会。
所以我想你最好的办法是找出遗留代码试图给你的时区的正确时区ID,然后从ZoneId.of(String)
获取你的ZoneId
。 或者稍微好一点,构建自己的别名映射,其中包含遗留代码中的ID或ID以及相应的现代ID,然后使用ZoneId.of(String, Map
。
自动化转换的一种可能尝试是迭代可用时区(通过TimeZone.getAvailableIDs()
和TImeZone.getTimeZone(String)
)并使用hasSameRules()
进行比较。 然后从ID字符串或从中获取的TimeZone
创建ZoneId
。
对不起,这不是你希望的答案。
这里我喜欢的解决方案因为它很简单,但你需要一个Date和一个TimeZone作为参数来检索ZoneId
private ZoneId getZoneOffsetFor(final Date date, final TimeZone timeZone){ int offsetInMillis = getOffsetInMillis(date, timeZone); return ZoneOffset.ofTotalSeconds( offsetInMillis / 1000 ); } private int getOffsetInMillis(final Date date, final TimeZone timeZone){ int offsetInMillis = timeZone.getRawOffset(); if(timeZone.inDaylightTime(date)){ offsetInMillis += timeZone.getDSTSavings(); } return offsetInMillis; }