Date.toString()在伦敦的时代增加了一个小时

时间:2019-01-24 19:41:31

标签: java date

如何获取Date.toString()以生成SimpleDateFormat可以针对1970年1月1日左右的日期正确解析的输出(我假设这也适用于1968年和1969年的冬天)

如果我运行以下命令,

System.out.println(TimeZone.getDefault());
Date date = new Date(0);
SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy");
Date date2 = sdf.parse(date.toString());
System.out.println("date: " + date);
System.out.println("date2: " + date2);
Date date3 = sdf.parse(date2.toString());
System.out.println("date3: " + date3);

此打印

sun.util.calendar.ZoneInfo[id="Europe/London",offset=0,dstSavings=3600000,useDaylight=true,transitions=242,lastRule=java.util.SimpleTimeZone[id=Europe/London,offset=0,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]]
date: Thu Jan 01 01:00:00 GMT 1970
date2: Thu Jan 01 02:00:00 GMT 1970
date3: Thu Jan 01 03:00:00 GMT 1970

问题在于伦敦是1970年1月1日在BST。所以正确的日期应该是

date: Thu Jan 01 01:00:00 BST 1970

date: Thu Jan 01 00:00:00 GMT 1970

但似乎两者混淆。

虽然我很想不支持java.util.Date,但这对我来说不是一个选择。

3 个答案:

答案 0 :(得分:4)

我可以引用https://bugs.openjdk.java.net/browse/JDK-6609362?jql=text%20~%20%22epoch%20gmt%22

  

请使用Z格式化和解析历史时区偏移量更改   为避免与历史时区名称更改混淆。


public class Test {
    public static void main(String[] args) throws ParseException {
        TimeZone.setDefault(TimeZone.getTimeZone("Europe/London"));
        SimpleDateFormat f = new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy");
        Date d = new Date(0);
        for (int i = 0; i < 10; i++) {
            String s = f.format(d);
            System.out.println(s);
            d = f.parse(s);
        }
    } } ```

答案 1 :(得分:4)

tl; dr

您的输入无效,因为BST(英国夏令时)在冬季无效。

BST无法可靠地解析,因为它是非标准的非唯一伪区域。

无需弄乱SimpleDateFormat。让现代的 java.time 类完成繁重的工作。

  

尽管我很想不支持java.util.Date,但对我来说这不是一个选择。

在代码的边缘,转换为旧类和现代类

// Convert from legacy to modern.
Instant instant = myJavaUtilDate.toInstant() ;

// Convert from modern to legacy.
java.util.Date myJavaUtilDate = Date.from( instant ) ;

冬天没有BST

BST中的“ B”显然是英国人。但是BST在此上下文中表示British Summer Time。这意味着Daylight Saving Time (DST)处于夏季,而不是冬季。因此,您在一月份的输入字符串中加上BST简直是

双夏时制

您在1970年使用英国时区的时刻的例子进一步复杂化了。

在英国,夏令时使用标准时间的夏令时在夏令时(UTC)(+01:00)之前的一小时,冬季在标准时间在夏令时的零(+00:00)的做法是 current < / em>练习。并非总是如此。

早在1970年,英国就是trialling a “double-summertime”。在1968-1971年的实验中,冬天的时间比UTC早了小时,而不是零,夏天的时间比UTC早了两个小时,而不是现在的一小时。 。这使英国人的时间与欧洲大陆更加相同,并希望减少事故的发生。

因此,如果我们调整1970年1月的某个时刻,则可以预期在时区Europe/London之前跳到一小时。尽管在2019年1月这一刻,我们预计不会出现跳跃,但英国的一天中的时间将与UTC相同(相对于UTC的偏移量为零小时-分钟-秒)。

避免伪区域

避免使用这些2-4个字符的伪区域,例如BST。它们不是标准化的。它们甚至都不是唯一的!因此BST可以解释为与British Summer Time一样的时区Pacific/Bougainville

Continent/Region的格式指定proper time zone name,例如America/MontrealAfrica/CasablancaPacific/Auckland。切勿使用2-4个字母的缩写,例如BSTESTIST,因为它们不是真正的时区,不是标准化的,甚至不是唯一的( !)。

转换

您可以轻松地在传统日期时间类和现代日期时间类之间进行转换。新的转换方法已添加到旧类中。按the naming conventions查找fromtovalueOf方法。

  • java.util.Datejava.time.Instant
  • java.util.GregorianCalendarjava.time.ZonedDateTime
  • java.sql.Datejava.time.LocalDate
  • java.sql.Timestampjava.time.Instant

转换

您在1970年1月1日的00:00输入字符串恰好是传统和现代日期时间类都使用的epoch reference date。我们对此值有一个常数。

Instant epoch = Instant.EPOCH ;
  

instant.toString():1970-01-01T00:00:00Z

在您的Europe/London时区看到同一时刻。

ZoneId z = ZoneId.of( "Europe/London" ) ;
ZonedDateTime zdt = epoch.atZone( z ) ;
  

zdt.toString():1970-01-01T01:00 + 01:00 [欧洲/伦敦]

请注意,上述 Double-Summertime 实验即已生效。如果我们在2019年尝试相同的代码,则offset-from-UTC为零。

ZonedDateTime zdt2019 = 
    Instant
    .parse( "2019-01-01T00:00Z" )
    .atZone( ZoneId.of( "Europe/London" ) )
;
  

zdt2019.toString():2019-01-01T00:00Z [欧洲/伦敦]

要转换为java.util.Date,我们需要一个java.time.Instant对象。 Instant代表UTC的时刻。我们可以从Instant对象中提取ZonedDateTime,从而有效地将区域调整为UTC。相同的时刻,不同的时钟时间。

Instant instant = zdt.toInstant(): 

现在应该回到1970年1月1日T00:00:00Z的纪元。

  

instant.toString():1970-01-01T00:00:00Z

要获取java.util.Date对象,您可能需要与尚未更新为 java.time 类的旧代码进行互操作,请使用添加到旧类中的新Date.from方法。

java.util.Date d = Date.from( instant ) ; // Same moment, but with possible data-loss as nanoseconds are truncated to milliseconds.
  

d.toString():格林尼治标准时间1970年1月1日00:00:00

顺便说一句,当从Instant转换为Date时,请注意可能的数据丢失。现代类的分辨率为nanoseconds,而旧类的分辨率为milliseconds。因此,您的小数秒的一部分可能会被截断。

查看run live at IdeOne.com以上的所有代码。

要转换另一个方向,请使用Date::toString方法。

Instant instant = d.toInstant() ;

ISO 8601

避免使用自定义格式的文本来交换日期时间值。将日期时间值序列化为可读文本时,请仅使用标准的ISO 8601格式。 java.time 类默认使用这些格式。

您正在尝试进行解析的那些字符串是糟糕的格式,因此切勿用于数据交换。


关于 java.time

java.time框架已内置在Java 8及更高版本中。这些类取代了麻烦的旧legacy日期时间类,例如java.util.DateCalendarSimpleDateFormat

目前位于Joda-Timemaintenance mode项目建议迁移到java.time类。

要了解更多信息,请参见Oracle Tutorial。并在Stack Overflow中搜索许多示例和说明。规格为JSR 310

您可以直接与数据库交换 java.time 对象。使用符合JDBC driver或更高版本的JDBC 4.2。不需要字符串,不需要java.sql.*类。

在哪里获取java.time类?

ThreeTen-Extra项目使用其他类扩展了java.time。该项目为将来可能在java.time中添加内容提供了一个试验场。您可能会在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

答案 2 :(得分:2)

这就是为什么我讨厌日期库。

暗示,BST应该在夏季使用,日历也将其定义为这样。

sun.util.calendar.ZoneInfo[id="Europe/London",offset=0,dstSavings=3600000,useDaylight=true,transitions=242,lastRule=java.util.SimpleTimeZone[id=Europe/London,offset=0,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]]

除了The adventures of year-round British Summer Time!的TIL以外

  

在1966-67年间的进一步调查促使哈罗德·威尔逊(Harold Wilson)政府引入了英国标准时间实验,英国全年仍保留在GMT + 1上。 1968年10月和1971年10月31日,当时恢复了以前的安排。

如果您在此期间测试日期,则每次解析都会发现日期漂移一个小时,直到转换点为止。

欧洲/伦敦的时间代码是格林尼治标准时间,使用夏令时可以节省夏令时。

Date的toString方法通过删除夏令时以选择要打印的时区来“标准化”输出。选项为GMTBST。欧洲/伦敦时间01:00:00被打印为01:00:00 GMT (即使格林威治标准时间+1是格林尼治标准时间+ 1)。因此,换句话说,date.toString()在该时期几乎无法正常工作,因为它使用GMT作为表面上不是GMT / CET的时区。 时间本身是正确的,但不是时区

我可以提出的“最简单”的解决方案从健全性检查点来看比较讨厌,但是可能会变得更加优雅。

private static final Date experimentEnd = new Date(1971-1900, 11-1, 11);
private static final Date experimentStart = (new Date(1968-1900, 10-1, 26));

private static boolean bstExperimentTime(Date date) {
    return date.after(experimentStart) && date.before(experimentEnd);
}

public static String forDateParsing(Date date) {
    if(bstExperimentTime(date))
        return date.toString().replace("GMT", "CET");
    return date.toString();
}

public static String forDatePrinting(Date date) {
     if(bstExperimentTime(date))
        return date.toString().replace("GMT", "BST");
    return date.toString();
}

您需要使用默认的“欧洲/伦敦”时区解析的任何日期都必须通过解析格式器来转换为GMT-> CET,这是正确的GMT + 1。

您需要使用默认的“欧洲/伦敦”时区进行打印的任何日期都必须通过解析格式器来转换为GMT-> BST,这是正确的显示。