日期格式‘YYYY-MM-DD’中的BUG

1. 日期格式

先来看一下日期格式主要有下面三种,US style,Euro style,RFC 3389。

Layout                      Format string    Example
------------------------    -------------    ----------
US style (Dec 29, 2019)     MM/DD/YYYY       12/29/2019
Euro style (29 Dec 2019)    DD/MM/YYYY       29/12/2019
RFC 3339 (2019-12-29)       YYYY-MM-DD       2019-12-29

2. 时间转换类

开发中,我们经常需要将时间进行转换成我们需要的格式,我们可以使用JDK8提供的一个DateTimeFormatter类来完成对日期的转换,如下面的代码:

public class CarefulWithThatDateEugene {
    private static void tryit(int Y, int M, int D, String pat) {
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pat);
        LocalDate dat = LocalDate.of(Y, M, D);
        String str = fmt.format(dat);
        System.out.printf("Y=%04d M=%02d D=%02d " +
                "formatted with " +
                "\"%s\" -> %s\n", Y, M, D, pat, str);
    }

    public static void main(String[] args) {
        tryit(2020, 01, 20, "MM/DD/YYYY");
        tryit(2020, 01, 21, "DD/MM/YYYY");
        tryit(2020, 01, 22, "YYYY-MM-DD");
    }
}

上面的代码执行之后,可以完美的输出:

Y=2020 M=01 D=20 formatted with "MM/DD/YYYY" -> 01/20/2020
Y=2020 M=01 D=21 formatted with "DD/MM/YYYY" -> 21/01/2020
Y=2020 M=01 D=22 formatted with "YYYY-MM-DD" -> 2020-01-22

2.1 错误的DD

但是如果我们执行下面的代码去测试:

tryit(2020,05,17,"MM/DD/YYYY");
tryit(2020,05,18,"DD/MM/YYYY");
tryit(2020,05,19,"YYYY-MM-DD");

你会发现控制台会抛出异常

Exception in thread "main" java.time.DateTimeException: Field DayOfYear cannot be printed as the value 138 exceeds the maximum print width of 2
	at java.time.format.DateTimeFormatterBuilder$NumberPrinterParser.format(DateTimeFormatterBuilder.java:2548)
	at java.time.format.DateTimeFormatterBuilder$CompositePrinterParser.format(DateTimeFormatterBuilder.java:2179)
	at java.time.format.DateTimeFormatter.formatTo(DateTimeFormatter.java:1746)
	at java.time.format.DateTimeFormatter.format(DateTimeFormatter.java:1720)
	at com.liuyao.time.CarefulWithThatDateEugene.tryit(CarefulWithThatDateEugene.java:15)
	at com.liuyao.time.CarefulWithThatDateEugene.main(CarefulWithThatDateEugene.java:26)

提示说Day的值为138,超过了日期的最大长度2,为什么日期会变成138呢,如果我们不管异常,输出的结果将会是:

Y=2020 M=05 D=17 formatted with "MM/DD/YYYY" -> 05/138/2020
Y=2020 M=05 D=18 formatted with "DD/MM/YYYY" -> 139/05/2020
Y=2020 M=05 D=19 formatted with "YYYY-MM-DD" -> 2020-05-140

可见日期都不对了,因为 DD代表的并不是一个月的某一天,而是一年的某一天,所以才会出现138超过31的值。

所以我们应该使用 dd来转换日期。

2.2 错误的YYYY

然后继续执行代码:

tryit(2018,12,30,"YYYY-MM-dd");
tryit(2018,12,31,"YYYY-MM-dd");
tryit(2019,01,01,"YYYY-MM-dd");

输出的结果是:

Y=2018 M=12 D=30 formatted with "YYYY-MM-dd" -> 2019-12-30
Y=2018 M=12 D=31 formatted with "YYYY-MM-dd" -> 2019-12-31
Y=2019 M=01 D=01 formatted with "YYYY-MM-dd" -> 2019-01-01

可见前面两个的2018年变成了2019年,年份变了。

这是为什么呢?因为YYYY使用的基于周的年份,而不是基于天数的,会计人员可以使用 YYYY来避免两年的日期分割,从而方便的计算工资等, 它的转换采用下面的规则:

  • The first day of every week is Monday.(每周的第一天是星期一)
  • If a week is split at the end of the year then it is assigned to the year in which more that half of the days of that week occur.(如果一个星期在年底被分割,那么它被分配到一年中超过一半的一个星期发生)

由于上面第二条的限制:

Sun 2015-12-27  -> Payroll week 52 of 2015

Mon 2015-12-28  -> Payroll week 53 of 2015 
Tue 2015-12-29  -> Payroll week 53 of 2015 
Wed 2015-12-30  -> Payroll week 53 of 2015
Thu 2015-12-31  -> Payroll week 53 of 2015 
-------------NEW YEAR---------------------
Fri 2016-01-01  -> Payroll week 53 of 2015
Sat 2016-01-02  -> Payroll week 53 of 2015
Sun 2016-01-03  -> Payroll week 53 of 2015

Mon 2016-01-04  -> Payroll week 01 of 2016

2015年,第52周之后还剩下四天,因此2016年的前三天被“放到”到2015年的工资单中。

但在2025年,情况刚好相反,到2025年底只剩下三天时间,就会被“放进”到2026年的工资单年份

Sun 2025-12-28  -> Payroll week 52 of 2025 
 
Mon 2025-12-29  -> Payroll week 01 of 2026 
Tue 2025-12-30  -> Payroll week 01 of 2026 
Wed 2025-12-31  -> Payroll week 01 of 2026 
-------------NEW YEAR---------------------
Thu 2026-01-01  -> Payroll week 01 of 2026 
Fri 2026-01-02  -> Payroll week 01 of 2026 
Sat 2026-01-03  -> Payroll week 01 of 2026 
Sun 2026-01-04  -> Payroll week 01 of 2026
 
Mon 2026-01-05  -> Payroll week 02 of 2026 

所以你如果采用的是 YYYY来格式化的年份,那么你讲不可避免的会在一年的结束或者一年的开始遇到这个问题,除非某年的第一天刚好是星期一,这样ISO-8601就会把日期分割的刚刚好。

下面就是活生生的例子:

日期格式‘YYYY-MM-DD’中的BUG_第1张图片

3. 正确使用

采用 DD来格式化代码,你会很容易发现这个错误,因为一年有85%的时间都是错误的,但是你如果采用了 YYYY这个错误的 格式来格式年份,一年中只有1%的时间是错误的,而且这个错误还不是每7年就会出现一次的。

  • 正确的格式化ISO-8601模板为: yyyy-MM-dd HH:mm:ss

  • 正确的UTC的时间模板为 yyyy-MM-dd'T'HH:mm:ss.SSSXXX

4. 参考链接

  1. Serious Security: The decade-ending “Y2K bug” that wasn’t
  2. DateTimeFormatter
  3. 你今天因为 YYYY-MM-dd 被提 BUG 了吗

你可能感兴趣的:(Java基础)