解析 Java 中的日期时间字符串是否需要“Locale”?

Is `Locale` needed for parsing date-time strings in Java?

有时我会看到问题与解答,其中需要区域设置来解决解析问题。然而在其他人中没有提到 Locale。

地区和时区无关

语言环境和时区是分开的,orthogonal 关于 date-time 处理的问题。

  • 语言环境
    • 语言
      人类语言,例如Arabic, French, Farsi. Text of the names of day-of-week, names of month, and ordinal indicators。比如……是Monday还是Lundi
    • 文化
      排列构成 date-time 值的字符串表示的文本和数字片段时常用的习语。例如……简写为month-date-yeardate-month-yearyear-month-date?在长格式中,星期几先来吗?月份名称是否有首字母大写或全部小写?缩写是否有 FULL STOP (PERIOD) 个字符?
  • 时区
    • Offset
      wall-time used by people in one area from UTC (GMT)之间的小时数和分钟数相差wall-time used by people in one area from UTC (GMT),世界上调节时钟和时间的主要时间标准。
    • 异常
      偏移量更改的历史记录,当前应用的定义偏移量的规则,包括 Daylight Saving Time (DST) 等调整,以及已确认的计划在 near-future.
    • 中进行更改

因此您可以 mix-and-match 语言环境和时区。下面是一些示例。

  • 一位在印度浦那参加会议的法国人需要查看印度 wall-time 的会议日程,但更愿意将 "Monday" 阅读为 "Lundi",他的母语是法语。
    • 法语语言环境
    • 印度时区
  • 一位在西雅图工作的巴西工程师想观看来自芬兰图尔库的网络研讨会直播。她需要知道何时将网络浏览器指向网络研讨会。在调整到西雅图时区后,她需要知道芬兰的开始时间,但需要用她的母语葡萄牙语显示巴西的语言环境。
    • Locale( "pt" , "BR" ) 用于演示(用于生成文本表示)
    • 芬兰的预定开始时间必须从 Europe/Helsinki 调整为 America/Los_Angeles(西雅图时区)。
  • 冰岛的一家报纸可能会将在俄罗斯发生的事件报道为两个 date-time 秒,即莫斯科时区,并为清楚起见添加 UTC 时区。但是这篇文章将使用冰岛语作为文本,包括 day-of-week。
    • 莫斯科时区和冰岛地区
    • UTC 时区和冰岛地区

当 parsing/generating 字符串是 date-time 值的文本表示时,区域设置仅在两种情况下使用:

  • day-of-week 的名称,and/or 月份的名称(或序号指示符,但最好避免这些)
  • Soft-coded,本地化格式

在第一种情况下,如果您的字符串包含 "Monday"/"Lundi" 或 "March"/"Mars" 之类的词,则使用 Locale 进行翻译那些字符串。

在第二种情况下,如果您没有明确的格式化模式,则使用语言环境来了解 day-of-week、日期、name-of-month 部分的预期顺序年,依此类推。例如,English-speaking 美国人说 "October 11",French-speaking 加拿大人使用 reverse-order “11 octobre”。 soft-coded,我们的意思是 DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL ) versus a hard-coded format like DateTimeFormatter.ofPattern("yyyy MM dd, EEE")

那么什么时候不需要 Locale 呢?如果您有一个全数字的输入字符串,例如“2015-01-23”,并且您 hard-coding 格式为 "yyyy-MM-dd"…

String input = "2015-01-23";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyy-MM-dd")

...那么 Locale 实际上是无关紧要的。您没有要翻译的词,没有 "Monday" 或 "Lundi"。并且不要求使用需要语言环境才能知道日期是在月份之前还是之后的本地化格式化程序,以及其他此类详细信息。

请注意,在这种情况下您仍然可以指定语言环境。实际上,我建议您养成 始终指定区域设置(以及时区)的习惯。

隐式语言环境和时区

那么,为什么您会在 Whosebug 上看到这么多 date-time 相关的问题和答案,却没有任何语言环境?因为如果省略,JVM 的当前默认语言环境将自动且静默地应用。

因此,如果您在设置为美国语言环境的 JVM 上有一个带有英文文本 运行 的字符串,那么没问题。但是不推荐这种对隐式语言环境的依赖。如果任何应用程序的任何线程中的任何代码 在运行时 调用 Locale.setDefault,并影响该 JVM 中的所有其他代码。然后你的代码抛出异常。最好养成明确指定 expected/desired 语言环境的习惯。

相同的时区建议。如果省略,JMV 的当前默认时区将自动且静默地应用。同样,任何应用程序的任何线程中的任何代码 在运行时 期间都可以调用 TimeZone.setDefault,并影响该 JVM 中的所有其他代码。然后您的代码抛出异常或出现意外行为。

Surprise-changes-at-runtime 应该足以养成始终指定语言环境和时区的习惯。但另一个好处是它还使您的代码 self-documenting。此外,在编程时有意识地指定语言环境和时区可能会提醒您注意不正确或未经证实的假设。

示例场景

想象一下魁北克的商人。她向土耳其的一位客户确认了他的交货时间,交货时间为中午。因此,她使用 wall-time 创建了一个 object,将在土耳其接受交货。

ZoneId zoneIdIstanbul = ZoneId.of( "Europe/Istanbul" );
ZonedDateTime zdtIstanbul = ZonedDateTime.of( 2015, 10, 11, 12, 30, 00, 0, zoneIdIstanbul );  // Half-past noon in Turkey.

为了客户的方便,她使用 Turkish language 和习惯来格式化文本。她定义了一个格式化程序 object 来处理 date-time 值的文本表示的生成。我们还可以为格式化程序分配一个时区,以便在生成文本表示时应用。但是 ZonedDateTime object 已经分配了一个时区,因此格式化程序将在该时区进行选择。

Locale locale_tr_TR = new Locale( "tr", "TR" );
DateTimeFormatter formatter_tr_TR = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL ).withLocale( locale_tr_TR );
String outputTurkish = formatter_tr_TR.format( zdtIstanbul );

我们的业务员知道客户在芬兰使用物流协调员,因此她在 Finnish. So we have a Turkey time zone 中使用芬兰语言环境打印了相同的 date-time 值。

Locale locale_fi_FI = new Locale( "fi", "FI" );
DateTimeFormatter formatter_fi_FI = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL ).withLocale( locale_fi_FI );
String outputFinnish = formatter_fi_FI.format( zdtIstanbul );

对于她自己,她需要一个包含预期交付的字符串 wall-time 这样她就可以设置一个警报来提醒她检查是否成功完成。她母语是法语,而不是土耳其语。

因此下一段代码的不同之处在于我们需要调整时区 语言环境。时间线中的同一时刻,预期交付的 date-time,但在文本中表示不同。请注意这次我们如何在创建格式化程序的链的末尾添加对 withZone 的额外调用,我们在其中指定时区调整以覆盖 ZonedDateTime object 的分配区域。

Locale locale_fr_CA = Locale.CANADA_FRENCH;
ZoneId zoneId_Montréal = ZoneId.of( "America/Montreal" );
DateTimeFormatter formatter_fr_CA_Adjusted = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL ).withLocale( locale_fr_CA ).withZone( zoneId_Montréal );
String outputQuébec = formatter_fr_CA_Adjusted.format( zdtIstanbul );

最后,为了我们的 English-speaking readers of whosebug.com, let's do a version in English. But note that we recycle the Québec formatter, keeping the already-set time zone but replacing the locale to that of United States. (Technically not recycling, but so to speak. Use of immutable objects 意味着新的 object 是用基于旧的 object 的值实例化的。)

Locale locale_en_US = Locale.US;
DateTimeFormatter formatter_US_Unadjusted = formatter_fr_CA_Adjusted.withLocale( locale_en_US );
String output_US_Unadjusted = formatter_US_Unadjusted.format( zdtIstanbul );

让我们看看这些值的输出。转储到控制台。

首先我们隐式调用toString method on our ZonedDateTime object. This method by default uses one of the standard formats defined by ISO 8601。但是 java.time 通过在方括号 [Europe/Istanbul] 中附加时区名称来扩展该格式。交换数据时,请使用这些明确的标准格式,而不是任何 human-friendly 格式。

System.out.println( "zdtIstanbul : " + zdtIstanbul );
System.out.println( "outputTurkish : " + outputTurkish );
System.out.println( "outputFinnish : " + outputFinnish );
System.out.println( "outputQuébec : " + outputQuébec );
System.out.println( "output_US_Unadjusted : " + output_US_Unadjusted );

输出结果告诉我们,土耳其的午餐时间送货意味着 5:30 我们在魁北克的女士的上午警报。

zdtIstanbul : 2015-10-11T12:30+03:00[Europe/Istanbul]
outputTurkish : 11 Ekim 2015 Pazar 12:30:00 EEST
outputFinnish : sunnuntai, 11. lokakuuta 2015 12.30.00 EEST
outputQuébec : dimanche 11 octobre 2015 5 h 30 EDT
output_US_Unadjusted : Sunday, October 11, 2015 5:30:00 AM EDT