好奇的Naked Security读者向我们发出警告,他们认为Java日期处理中可能是“类似Y2K的错误”。
引起警报的原因是一个Twitter帖子,该帖子以头条推文开头说:“ PSA:这是检查您的格式者的季节,人们。”
PSA:这是检查
格式的季节,直到Java的DateTimeFormatter模式“ YYYY”为您提供基于周的年份(默认情况下为ISO-8601标准),该星期的星期四。
12/29/2019格式至2019
12/30/2019格式至2020
-Giuliana Taylor(@NmVAson),2019年12月20日
正如@NmVAson所指出的那样,当您要求JavaDateTimeFormatter库告诉您当前YYYY的(通常的程序员缩写)意思是“以四位数字表示的年份”时,问题就来了。
例如,当程序员缩写世界上常用的日期格式时,他们经常使用格式字符串来表示所需的布局,如下所示:
布局格式字符串示例
------------------------ ------------- ----------
美式(2019年12月29日)MM / DD / YYYY 12/29/2019
欧式风格(2019年12月29日)DD / MM / YYYY 2019/12/12
RFC 3339(2019-12-29)YYYY-MM-DD 2019-12-29
实际上,许多编程语言都提供了代码库,可帮助您使用上述格式字符串来打印日期,以便您可以自动调整软件的输出以适合每个用户的个人喜好。
这里的问题是,有许多不同的日期处理功能,例如GetDateFormatEx()在Windows,strftime()Unix和Linux系统上,一直到Java的全能,全舞DateTimeFormatter模块。
上面提到的Java库以及其他功能,使您可以方便地使用上面显示的三个字符串设置日期的格式,从而得到如下所示的合理结果:
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.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
到现在为止还挺好!
但是,如果您在年中尝试此操作,则会得到:
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
一个容易发现的错误
什么?!?
请注意,尽管一年中最长的月份只有31天,但奇怪的日期数字远大于31。
这样一来,您就可以回到文档,或者至少回到您最喜欢的搜索引擎,在其中粗略浏览一下就会发现该缩写DD实际上是一年中的某天而不是月份中的某天。
因此DD,dd仅在1月份产生相同的答案,此后一年的日期变为32,而2月的第一天的月份则重置为01。(要清楚,在除夕-一年的最后一天,12月31日-一年中的一天是365,or年是366,而当月的一天是31。)
换句话说,即使对1月以外的日期进行粗略的测试也会显示此格式字符串错误,因此很少有人这样做。
您需要的是格式字符串dd,如下所示:
Y=2020 M=05 D=17 formatted with "MM/dd/YYYY" -> 05/17/2020
Y=2020 M=05 D=18 formatted with "dd/MM/YYYY" -> 18/05/2020
Y=2020 M=05 D=19 formatted with "YYYY-MM-dd" -> 2020-05-19
难以发现的错误
除非您仍然错,YYYY否则不代表“基督教数字的四位数年份”。
这在Java库(以及其他全脂日期数据库)中也表示为小写文本string yyyy。
相比之下,YYYY表示所谓的基于周的年份,会计所依赖的东西是避免在不同的两年之间分配周数,从而避免公司的薪水分配。
基于本周年数和基督教时代的年数几乎都是一样的,所以很容易看从Java的几个输出 DateTimeFormatter模块,并认为他们是始终不变的...
…但是你会犯错误的危险。
对于农民,牧师,天文学家和商人而言,不方便的是,太阳年不会精确地分为几天,因此也不能整齐地分为几周或几个月。(阴历月份与太阳年也不相称,这使事情变得更加复杂。)
每个簿记员都知道,一年中并不完全有52周,因为最后总是剩下一两天。
这是因为一年(或a年)中有365(或366)天的事实;一周有7天;并且365/7 = 52余数1(或366/7 = 52余数2)。
因此,为了会计上的方便,通常将某些年视为具有52个整周,而另一些则具有53个星期,从长远来看,这会使每周的收入计划和每周的工资单保持平衡。
换句话说,在某些年份中,“工资周01”实际上是在元旦之前开始的。在其他年份,直到新年第一周的几天才开始。
有一个标准
在ISO-8601日历系统中定义了一个标准,在Java文档中将其描述为“当今世界大多数地方使用的现代民用日历系统”。
ISO-8601作了一些假设,包括:
- 每周的第一天是星期一。
- 如果在年末拆分一周,则将其分配给该周中有一半以上的日子发生的年份。
第二个假设似乎是合理的,因为这意味着在正确的年份中,您的工资日总是比错误的年份多。
例如,对于2015年,在第52周之后还剩下四天,因此2016年的前三天被“吸回”到2015工资年度:
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
但是到了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
即将发生大日期错误!
你能看到这是怎么回事吗?
如果你已经有了一个日期格式字符串类似MM/dd/YYYY或YYYY-MM-dd在任何软件的任何点在您使用的ISO-8601的日期格式库...
…您不可避免地会遇到错误,这些错误会在一年的结尾或下一年的开始打印出错误的年份的日期,除非在元旦是星期一的年份。
(当12月31日为星期日,而1月1日为星期一时,ISO-8601“周拆分”过程将正常进行,到年底还剩0天。)
如果使用YYYY应该写的位置yyyy,则您的日期将有规律但很少出错,因此即使您可能不容易注意到它们,您的代码也会出错。
以下是您在2018年看到的过时日期:
Y=2018 M=12 D=30 formatted with "YYYY-MM-dd" -> 2018-12-30 +correct+
Y=2018 M=12 D=31 formatted with "YYYY-MM-dd" -> 2019-12-31 *WRONG* (one year ahead)
-------------------------------NEW YEAR------------------------------
Y=2019 M=01 D=01 formatted with "YYYY-MM-dd" -> 2019-01-01 +correct+
对于2019年:
Y=2019 M=12 D=28 formatted with "YYYY/MM/dd" -> 2019/12/28 +correct+
Y=2019 M=12 D=29 formatted with "YYYY-MM-dd" -> 2019-12-29 *WRONG* (one year ahead)
Y=2019 M=12 D=30 formatted with "YYYY-MM-dd" -> 2020-12-30 *WRONG* (one year ahead)
Y=2019 M=12 D=31 formatted with "YYYY-MM-dd" -> 2020-12-31 *WRONG* (one year ahead)
-------------------------------NEW YEAR------------------------------
Y=2020 M=01 D=01 formatted with "YYYY-MM-dd" -> 2020-01-01 +correct+
2020年:
Y=2020 M=12 D=31 formatted with "YYYY-MM-dd" -> 2020-12-31 +correct+
-------------------------------NEW YEAR------------------------------
Y=2021 M=01 D=01 formatted with "YYYY-MM-dd" -> 2020-01-01 *WRONG* (one year behind)
Y=2021 M=01 D=02 formatted with "YYYY-MM-dd" -> 2020-01-02 *WRONG* (one year behind)
Y=2021 M=01 D=03 formatted with "YYYY-MM-dd" -> 2020-01-03 *WRONG* (one year behind)
Y=2021 M=01 D=04 formatted with "YYYY-MM-dd" -> 2021-01-04 +correct+
如果可以通过@NmVAson启动Twitter线程,那么很多程序员似乎仍然会犯这种错误,这意味着他们没有很好地测试他们的代码。
正如我们上面提到的,DD用错误而不是错误的书写dd似乎是一个不寻常的错误,大概是因为该错误在一年中的出现率约为85%,并且由于三位数的天数而在一年中的出现率高达70%以上。
可以肯定的是,YYYY错误地写错误而不是yyyy在一年中不到1%的日期中产生错误,而且不是每7年中的任何一年都发生错误,但是即使错误率低于1%,也确实没有任何借口未能发现您犯了这个错误。
您可能会找借口,说没有碰到一个2分之32的错误是倒霉。您甚至可能会因“运气不好”而逃脱,以为错误率高达百万分之一…
…但是只有1%(特别是当那些以百分比为中心的年份恰好在年末时)时,您真的不应该让这种错误逃脱您的注意。
该怎么办?
如果您是负责处理日期的代码的程序员或项目经理,并且几乎可以肯定需要执行任何类型的日志记录的任何工作,那么请确保您:
- 不要做假设。仅仅因为大写YYYY表示某些地方的日历年并不意味着它总是如此。
- 阅读完整的手册,或简称RTFM。遗憾的是,针对ISO-8601的TFM非常复杂,但这应该是您的问题,而不是用户的问题-动力来自责任。
- 正确检查您的代码。请记住,审稿人也需要进行RTFM。
- 彻底测试您的代码。YYYY遇到ISO-8601错误的人实际上并没有一个好的测试集,因为该错误大约在每7年的6年末出现一次。
我们认为日历是理所当然的,但日历的设计和使用是艺术与科学的融合,已经持续了数千年,仍然需要极大的关注和关注。
特别是在软件中!