Java 即使提供了浏览器时区,将字符串转换为日期也会减少一天

Java Converting String to Date resulting one day less even though browser time zone provided

我将 Angular 应用程序的日期作为字符串发送到服务器并转换为 java Date 对象以存储在数据库中。

同时从 UI 发送 timeZoneOffset 以在转换时使用客户的时区。 (谷歌搜索后我发现这种方法可以根据用户位置获得正确的结果)

编写如下代码进行转换:

public static void main(String args[]) throws ParseException {
    String inputDate = "04/05/2018"; // This date coming from UI
    int timeZoneOffset = -330; // This offset coming from UI 
                               // (new Date().getTimeZoneOffset())
    getDate(inputDate, timeZoneOffset);
}

public static Date getDate(String inputDate, int timeZoneOffset)
        throws ParseException {
    SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy");
    ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(-timeZoneOffset * 60);
    System.out.println("Default time zone: " + TimeZone.getDefault().getID());
    TimeZone timeZone = TimeZone.getTimeZone(zoneOffset);
    System.out.println("Time zone from offset: " + timeZone.getID());
    dateFormat.setTimeZone(timeZone);
    Date date = dateFormat.parse(inputDate);
    System.out.println("Converted date: " + date);
    return date;
}

预期输出:

Default time zone: America/New_York
Time zone from offset: GMT+05:30
Converted date: Thu April 5 00:00:00 IST 2018

服务器中的实际结果

Default time zone: America/New_York
Time zone from offset: GMT+05:30
Converted date: Wed April 4 14:30:00 EDT 2018

为什么我设置了用户时区,日期还是减到一天?我是日期和时间相关概念的新手,我用谷歌搜索了几次都没有找到答案,有人可以帮忙吗。

提前致谢

不是减少了一天,而是减少了11.5小时。这恰好是 GMT+05:30 和 "America/New_York" 之间的时差,即 GMT-04:30 或 GMT-05:30(取决于一年中的时间)。

GMT+05:30 在印度的某个地方,我认为,因为那是唯一使用 30 分钟偏移而不是整小时偏移的地方。在印度是 4 月 5 日,在纽约仍然是 4 月 4 日。

问题可能是您没有从客户那里得到时间,所以它会假定为午夜。如果要进行时区转换,最好包括实际时间。

正确。

tl;博士

LocalDate.parse( 
    "04/05/2018" ,
    DateTimeFormatter.ofPattern( "MM/dd/uuuu" ) 
)
.atStartOfDay(
    ZoneId.of( "Asia/Kolkata" ) 
)
.toString()

2018-04-05T00:00+05:30[Asia/Kolkata]

要在您的数据库中存储,请使用 UTC。

当新的一天在印度开始时,UTC 的日期仍然是“昨天”,因此是 4 月 4 日而不是 4 月 5 日。同一时刻,时间轴上的同一点,不同的 wall-clock 时间。

LocalDate.parse( 
    "04/05/2018" ,
    DateTimeFormatter.ofPattern( "MM/dd/uuuu" ) 
)
.atStartOfDay(
    ZoneId.of( "Asia/Kolkata" ) 
)
.toInstant()

2018-04-04T18:30:00Z

java.time

您使用的是可怕的旧 date-time classes,它们已被证明设计不佳、令人困惑和麻烦。它们现在被 java.time classes.

所取代

完全避免遗留 date-time classes

ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(-timeZoneOffset * 60);
…
TimeZone timeZone = TimeZone.getTimeZone(zoneOffset);

您将现代 classes (ZoneOffset) 与麻烦的遗留 classes (TimeZone) 混合在一起。 不要将现代 classes 与传统 classes 混在一起。 忘掉所有旧的 classes,包括 DateCalendarSimpleDateFormatjava.time classes 旨在完全取代旧版 classes。

而不是 TimeZone,使用 ZoneId(和 ZoneOffset)。

LocalDate

将您的输入字符串解析为 LocalDateLocalDate class 表示没有 time-of-day 和时区的 date-only 值。

String input = "04/05/2018" ;
DateTimeFormatter f = DateTimeFormatter.ofPattern( "MM/dd/uuuu" ) ;
LocalDate ld = LocalDate.parse( input , f ) ;

偏移量与时区

int timeZoneOffset = -330;

offset-from-UTC 不是 时区。偏移量只是从 UTC 偏移的小时数、分钟数和秒数。您选择的变量名称表明在这一点上可能存在混淆。

ZoneOffset offset = ZoneOffset.of( -3 , 30 ) ;  

A time zone 是特定地区的人们使用的偏移的过去、现在和未来变化的历史。所以时区总是比偏移量更可取。

指定 proper time zone name in the format of continent/region, such as America/Montreal, Africa/CasablancaPacific/Auckland。切勿使用 3-4 个字母的缩写,例如 ESTIST,因为它们 不是 真正的时区,不是标准化的,甚至不是唯一的(!)。

ZoneId z = ZoneId.of( "Asia/Kolkata" ) ;  // India time zone. Currently uses offset of +05:30 (five and a half hours ahead of UTC). 

当天的第一刻

您的目标似乎是那个区域的那个日期的第一刻。让 java.time 确定 first-moment-of-the-day。不要假设时间是 00:00:00。在某些日期的某些区域,这一天可能会在其他时间开始,例如 01:00:00.

ZonedDateTime zdt = ld.atStartOfDay( z ) ;  // Determine the first moment of the day on this date in this zone. Not always 00:00:00.

作为您为什么应该使用时区而不仅仅是 offset-from-UTC 的示例,请查看 -330 的示例数据,我很容易将其误解为比 UTC 晚三个半小时.此偏移量目前仅在区域 America/St_Johns 中使用,并且仅在一年中的部分时间使用。因此,如果您将 -03:30 的偏移量应用于一年中错误部分的日期,您的结果将无效但未被发现。

使用偏移量(不推荐)

但是您的示例缺少时区,所以让我们使用 offset-from-UTC 而不是时区。

您使用 int 整数来表示 offset-from-UTC 是一种糟糕的类型选择。首先,它是模棱两可的。 -330 可能会被解释为 -03:30 延迟三个半小时的笨拙尝试。其次,它使解析变得比需要的更棘手。第三,作为分钟数,它忽略了秒偏移的可能性。第四,尽管常见用法和标准用法相反,但您使用负数作为 UTC 的偏移量 ahead(显然)。最后,它忽略了 ISO 8601 为将偏移量表示为文本而设置的明确标准:±HH:MM:SS(和变体)。顺便说一句,填充零在标准中是可选的,但我建议始终包括在内,因为各种库和协议都需要它。

您的意图似乎是整数预期的分钟数。

long seconds =( TimeUnit.MINUTES.toSeconds( - 330 ) * -1 );  // Multiply by negative one to flip the sign to standard ISO 8601 usage, where `+` means “ahead* of UTC and `-` means *behind* UTC.

seconds: 19800

ZoneOffset offset = ZoneOffset.ofTotalSeconds( ( int ) seconds );

offset.toString(): +05:30

最后一步:在此偏移量中获取当天的第一个时刻。警告:我们不确定这个偏移量在这个日期是否有效,因为我们没有时区。

从返回的 ZonedDateTime 转换为 OffsetDateTime。如上所述,确定一天中的第一时刻应该始终使用时区来完成,从而得到 ZonedDateTime。我们违反了使用偏移量的明智做法,但使用返回的 ZonedDateTime 对象会产生误导,因为我们缺少真正的时区,并且只有一个偏移量。所以 OffsetDateTime class 让我们的意图更明确,我们的代码更 self-documenting.

    OffsetDateTime odt = ld.atStartOfDay( offset ).toOffsetDateTime();

同样,这种使用偏移的方法推荐,因为您应该从用户那里收集时区名称作为输入而不是偏移量。

UTC

通常最好以 UTC 格式存储时刻。

从您的 OffsetDateTimeZonedDateTime 中提取 Instant 以获得与 UTC 相同的时刻。

Instant instant = zdt.toInstant() ;

2018-04-04T18:30:00Z


关于java.time

java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

要了解更多信息,请参阅 Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310

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

从哪里获得java.time classes?

  • Java SE 8, Java SE 9,及以后
    • Built-in。
    • 标准 Java API 的一部分,带有捆绑实施。
    • Java 9 添加了一些小功能和修复。
  • Java SE 6 and Java SE 7
    • java.time 的大部分功能是 back-ported 到 Java ThreeTen-Backport 中的 6 和 7。
  • Android
    • Android 的更高版本 java.time classes.
    • 捆绑实施
    • 对于较早的 Android (<26),ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See .

ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.