1. 问题背景
每年临近年元旦的几天里,各位总会为一个跨年级别的Bug而发愁---明明是2019年12月31日,但显示的时候却出现了2020/12/31这样错误的日期显示*,甚至像微信提供的订阅号助手工具也出现了这样的错误。
这种情况在编程语言中并不罕见,特别是在处理时间和数字数据时,往往在平时运行良好,但在特定时间或特定环境下就会出现问题。经排查,是因为日期格式化时使用了"YYYY-MM-dd"作为格式化模板。每年都有人因为这个问题而在元旦当天被紧急召回去修改Bug,甚至有的同学因为这个Bug导致即将到手的年终奖飞了。
从各位大神分享的博客、文章中可以看出,使用Java的程序员最容易踩到这个坑 [1] [2],而前端似乎极少碰到这个问题。真的是这样吗?本文便带你一探究竟。
2. 问题根本原因
根本原因在某篇博客中 [1]已经提到,使用大写的YYYY
格式表示基于周的年份,即当前日期所在周所属的年份。根据这种格式,一周从周日开始,到周六结束,因此如果这一周跨年了,那么整个周将被归入下一年。
因此,回望上一部分提到的Bug,不难发现,2019年12月30日和31日均产生跨年,那么使用YYYY格式化时便会产生跨年Bug。
虽然问题分享博客 [1] [2]均提到了将YYYY
改为小写的yyyy
,但都没有指出其所以然。事实上,Unicode官网 3上已经明确指出大写YYYY
与小写yyyy
的区别:小写yyyy
指的是日历年,即该日属于哪一年就是哪一年,换句话说就是实际日期;大写YYYY
即前文提到的基于周的年份。此外, Unicode官网也贴心地指出了YYYY
与yyyy
在跨年上的不同。
结论:
- 小写的
yyyy
表示日历年,即和日历同步的日期,最为准确 - 大写的
YYYY
表示基于周的年份,可能会导致跨年问题
3. 问题处理方式及结论
上一小节详述了问题发生的根本原因,本小节则着重回答两个问题:
- 虽然这个问题在Java编程中经常碰见,但是前端就没有这个问题了吗?
- 前端如何治理与解决这个问题?
3.1 前端不会发生这个问题吗?
TLDR (太长不看):
- 常用库都不会出现这个问题
- 各类时间库对
yyyy
和YYYY
的处理上有所不同
正文部分:
本文选取四个较常用的日期处理库,调研其format方法实现方式,以获取对于大写YYYY
和小写yyyy
的处理方式。经调研,结论如下:
moment | dayjs | date-fns | luxon | |
---|---|---|---|---|
大写 YYYY
|
处理为日历年 | 处理为日历年 | 如不开启options.useAdditionalWeekYearTokens 则会报错,开启后处理为基于周的年份 |
不处理 |
小写 yyyy
|
处理为Era(纪年),对于日本时间适用(如“令和1年”)。如无纪年,则处理为日历年 | 不处理 | 默认方式,处理为日历年 | 处理为日历年 |
四个库对于获取年份的底层实现:
moment | dayjs | date-fns | luxon |
---|---|---|---|
判断是否为UTC,是则getUTCFullYear ,否则getFullYear
|
判断是否为UTC,是则getUTCFullYear ,否则getFullYear
|
默认为getUTCFullYear
|
一律使用getUTCFullYear
|
getFullYear
根据本地时间返回对应的年份,而getUTCFullYear
根据GMT时间返回对应的年份。例如,北京时间2024年1月1日凌晨1点,getFullYear
返回2024
,而getUTCFullYear
返回2023
,因为此时GMT时间仍是2023年12月31日下午5点。
那么,对于跨年时间,如2019-12-31
,getFullYear
与getUTCFullYear
是否会出现差错呢?用一段代码测试:
// 使用原生Date测试
const str = '2019-12-31';
const d = new Date(str);
console.log(d.getFullYear());
console.log(d.getUTCFullYear());
将测试代码运行结果如下图所示:
综上,我们可以看出:
-
moment
/dayjs
使用大写YYYY
表示正常日历年; -
date-fns
/luxon
使用小写yyyy
表示正常日历年。这也是date-fns
的默认行为。 - 只有
date-fns
在开启了options.useAdditionalWeekYearTokens
选项后才会将YYYY
处理为基于周的年份,否则在处理YYYY
时会抛出错误。 -
dayjs
不处理小写yyyy
,luxon
不处理大写YYYY
。 - 各个库的底层实现均为
getFullYear
和getUTCFullYear
,一般不会发生上一小节的跨年的错误。 - (重要) 使用
getUTCFullYear
时要注意时差。北京时间比GMT快8小时。
3.2 前端治理方案
综合前述,本人提出治理方案如下:
- 如项目中没有使用任何日期库,获取年份一般应直接使用
getFullYear
,仅在必须获取GMT年份时才可以使用getUTCFullYear
。 - 原则上禁止使用
getYear
[9],因为getYear
有2000年bug。 - (重要)如项目中使用了
moment
/dayjs
,仅使用YYYY
获取年份。 - 如项目中使用了
date-fns
,仅使用yyyy
获取年份。 - 如使用
date-fns
或luxon
,要十分小心时差问题。