使用Java,我有一个SimpleTimeZone
实例,其中包含来自遗留系统的GMT偏移量和夏令时信息。
我想检索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作为参数,如V.V.的答案@Ole所述)。然后ZoneId
类加载在 JVM 中配置的任何夏令时信息(它不保留与原始SimpleTimeZone
对象相同的规则)。
根据ZoneId
javadoc:如果区域ID等于"GMT",">UTC"或"UT",则结果是具有相同ID和规则的ZoneId,等同于ZoneOffset.UTC。而且ZoneOffset.UTC
根本没有DST规则。
因此,如果您想拥有一个具有相同 DST 规则的ZoneId
实例,则必须手动创建它们(我不知道这是可能的,但实际上是,请在下面检查)。
您的夏令时规则
查看SimpleTimeZone
javadoc,您创建的实例具有以下规则(根据我的测试):
- 标准偏移量为
+02:00
(UTC/GMT 提前 2 小时) - 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);
然后我用边界日期(在 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())));
输出为:
2017-01-01 00:00:00 +0200 2017-01-01 01:01:00 +0300 2017-02-05 00:00:00 +0300 2017-04-02 23:01:00 +0200
因此,它遵循上述规则(在 2017 年 1 月 1 日午夜偏移量为+0200
,一分钟后它是 DST(偏移量现在+0300
;相反的情况发生在 05/02(DST 结束,偏移量回到+0200
年))。
使用上述规则创建ZoneId
不幸的是,你不能扩展ZoneId
和ZoneOffset
,你也不能改变它们,因为两者都是不可变的。但是可以创建自定义规则并将其分配给新ZoneId
。
而且它似乎没有办法直接将规则从SimpleTimeZone
导出到ZoneId
,因此您必须手动创建它们。
首先我们需要创建一个ZoneRules
,一个包含偏移量何时以及如何变化的所有规则的类。为了创建它,我们需要构建一个包含 2 个类的列表:
ZoneOffsetTransition
:定义偏移更改的特定日期。必须至少有一个才能使其工作(空列表失败)ZoneOffsetTransitionRule
:定义一个一般规则,不限于特定日期(如"一月的第一个星期日,偏移量从 X 变为 Y">)。我们必须有 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<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);
我无法创建一个从午夜后第一毫秒开始的ZoneOffsetTransition
(它们实际上是在午夜开始的),因为秒的小数部分必须为零(如果不是,ZoneOffsetTransition.of()
抛出异常)。所以,我决定在过去(LocalDateTime.MIN
)设置一个日期,以免干扰规则。
但ZoneOffsetTransitionRule
实例的工作方式与预期完全一样(DST 在午夜后 1 毫秒开始和结束,就像SimpleTimeZone
实例一样)。
现在我们必须将此ZoneRules
设置为时区。正如我所说,ZoneId
不能扩展(构造函数不是公共的),ZoneOffset
也不能扩展(它是一个final
类)。我最初认为设置规则的唯一方法是创建一个实例并使用反射进行设置,但实际上 API 提供了一种通过扩展java.time.zone.ZoneRulesProvider
类创建自定义ZoneId
的方法:
// 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;
}
}
请记住,您不应将 ID 设置为"GMT"、"UTC"或任何有效 ID(您可以使用ZoneId.getAvailableZoneIds()
检查所有存在的 ID)。"GMT"和"UTC"是 API 内部使用的特殊名称,可能会导致意外行为。所以选择一个不存在的名称 - 我选择了MyNewTimezone
(没有空格,否则它会失败,因为如果名称中有空格ZoneRegion
会引发异常)。
让我们测试这个新时区。必须使用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
使用的规则相同):
00:00 +0300 2017-04-02 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/s,然后使用ZoneId.of(String, Map<String,String>)
。
自动化翻译的一种可能尝试是遍历可用时区(通过TimeZone.getAvailableIDs()
和TImeZone.getTimeZone(String)
获得),并使用hasSameRules()
与每个时区进行比较。然后从 ID 字符串或从中获取TimeZone
创建ZoneId
。
对不起,这不是你所希望的答案。
这是我更喜欢的解决方案,因为它很简单,但是您需要一个日期和一个时区作为参数来检索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;
}