最近看到一个帖子,表示有人以"YYYY-MM-dd"
格式化日期时,在2019-12-30
时出现2020-12-30
的BUG。
本文来简单分析一下为什么会出现这个情况。
根据JDK文档关于日期的定义,y
表示的是我们日常使用的年份,而Y
表示的是Week year
。
先了解几个知识点:
Week year
Week year
表示的是这个周所属的年份。
A week year is in sync with a
WEEK_OF_YEAR
cycle. All weeks between the first and last weeks (inclusive) have the same week year value. Therefore, the first and last days of a week year may have different calendar year values.来源:https://docs.oracle.com/javase/7/docs/api/java/util/GregorianCalendar.html#week_year
WEAK_OF_YEAR
指的是这一年所有的周,从第01周开始到该年最后一周。
要注意这个周不一定是自然周,所包含的日期也不一定全部都是当年的日期。
Values calculated for the
WEEK_OF_YEAR
field range from 1 to 53. The first week of a calendar year is the earliest seven day period starting ongetFirstDayOfWeek()
) that contains at leastgetMinimalDaysInFirstWeek()
) days from that year.
第01周
根据这份JDK文档,当 getFirstDayOfWeek()
is MONDAY
(2) and getMinimalDaysInFirstWeek()
is 4时,JAVA判断周日期的标准与ISO_8601兼容:
第01周有几个相互等效且兼容的描述:
一年中第一个星期四的星期(正式的ISO定义),
1月4日这一周,
起始年份中大部分(四天或以上)的第一周,以及
从12月29日至1月4日的星期一开始的一周。
按照JAVA文档中的定义,每年最开始的几天和最后的几天的Week year
不一定是当年的年份值,而是受到每年的第01周/最后一周的影响。
JAVA中判断周主要受到Calendar
对象的getFirstDayOfWeek()
和getMinimalDaysInFirstWeek()
这两个本地值的影响。
其中:
getFirstDayOfWeek()
指定一周的第一天,比如, 美国一周从SUNDAY
开始,法国则是MONDAY
。getMinimalDaysInFirstWeek()
一年第一周所需最小的天数。比如1表示只要包含第一天就算该年的第一周,而7表示只有完整的一周都在该年才算该年的第一周。
注意:真正影响我们格式化日期结果的是SimpleDateFormat
中的calendar
对象对应的值。
而通过打印这个simpleDateFormat.calendar
,我们看到:
1 | //JDK1.7 |
所以可以得出结论,JAVA默认只要次年的1月1日在在这个跨年周,那么本周所有日期的Week year
都是次年的(JDK1.7
)。
问题分析
有了以上知识,我们再看看2019-12-30
以YYYY
格式化为什么会出现问题:
先看一下这些日期对应的星期:
周日 | 周一 | 周二 | 周三 | 周四 | 周五 | 周六 |
---|---|---|---|---|---|---|
29 | 30 | 31 | 1 | 2 | 3 | 4 |
首先根据JDK默认的第01周
的定义,2020-01-01
所在的周为2020的第一周
,所以2019-12-29到2020-01-04
都属于是2020年的第01周
。
再根据YYYY
表示的是Week year
的结论,可以知道,当使用YYYY
格式化时,2019-12-29到2020-01-04
都会得到2020
。
1 | val calendar = Calendar.getInstance() |
而如果我们把第一周最小天数minimalDaysInFirstWeek
改为5
天,那么很明显这一周属于2020
年的天数(从周日到周一,只有1号到4号4天)不够5天,所以这一周被划归为2019
年的第53周
,2019-12-29到2020-01-04
的week year
都是属于2019
。
1 | val calendar = Calendar.getInstance() |
再比如下面这个示例中的2010-12-26
。
按照JDK1.7
默认算法,一周从周日(2010-12-26
)开始,并且当年的1月1日(2011-01-01
)所在周为该年第一周,所以2010-12-26到2011-01-01
都被划到了2011
年的第一周。
但如果按照ISO_8601的标准,一周从周一开始,并且起始年份包含的天数至少要有4
天:
则很明显2010-12-26
属于2010
年的51周
,而2010-12-27到2011-01-02
都属于2010
年的第52周
(属于2020年的只有2天,不满足第一周的条件)。
周一 | 周二 | 周三 | 周四 | 周五 | 周六 | 周日 |
---|---|---|---|---|---|---|
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | 1 | 2 |
总结
结合以上结论,我们可以看到,在JAVA中(JDK1.7
):
“YYYY”
表示Week year
每年最开始的几天和最后的几天的
Week year
不一定是当年的值,而是受到当年的第一周/最后一周的影响。JAVA周的判断与
simpleDateFormat.calendar.minimalDaysInFirstWeek
和simpleDateFormat.calendar.firstDayOfWeek
有关。而这两个值都属于本地化值,在国内可以简单理解为一年1月1日所在的周就是当年的第一周。
我们可以通过修改
minimalDaysInFirstWeek
和firstDayOfWeek
来更改YYYY
格式化的值。
附录
JDK中日期格式化的参数及含义(来自https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html#month):
Letter | Date or Time Component | Presentation | Examples |
---|---|---|---|
G |
Era designator | Text | AD |
y |
Year | Year | 1996 ; 96 |
Y |
Week year | Year | 2009 ; 09 |
M |
Month in year | Month | July ; Jul ; 07 |
w |
Week in year | Number | 27 |
W |
Week in month | Number | 2 |
D |
Day in year | Number | 189 |
d |
Day in month | Number | 10 |
F |
Day of week in month | Number | 2 |
E |
Day name in week | Text | Tuesday ; Tue |
u |
Day number of week (1 = Monday, …, 7 = Sunday) | Number | 1 |
a |
Am/pm marker | Text | PM |
H |
Hour in day (0-23) | Number | 0 |
k |
Hour in day (1-24) | Number | 24 |
K |
Hour in am/pm (0-11) | Number | 0 |
h |
Hour in am/pm (1-12) | Number | 12 |
m |
Minute in hour | Number | 30 |
s |
Second in minute | Number | 55 |
S |
Millisecond | Number | 978 |
z |
Time zone | General time zone | Pacific Standard Time ; PST ; GMT-08:00 |
Z |
Time zone | RFC 822 time zone | -0800 |
X |
Time zone | ISO 8601 time zone | -08 ; -0800 ; -08:00 |
参考资料
感谢这篇文章,让我推翻了上一次的结论,发现了真正的原因:JAVA中的SimpleDateFormat yyyy和YYYY的区别
在线显示本周是一年第几周的网站:What’s the Current Week Number?