Java 笔记之 YYYY 格式化日期
最近看到一个帖子,表示有人以"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_YEARcycle. 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_YEARfield 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,我们看到:
//JDK1.7
minimalDaysInFirstWeek:1
firstDayOfWeek:1 //SUNDAY所以可以得出结论,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。
val calendar = Calendar.getInstance()
val simpleDateFormat = SimpleDateFormat("YYYY yyyy MM dd")
calendar.set(2019, 12-1, 29)
println(simpleDateFormat.format(calendar.time))
//output
DATE:2019 12 29
minimalDaysInFirstWeek:1
firstDayOfWeek:1
YYYY yyyy MM dd
2020 2019 12 29而如果我们把第一周最小天数minimalDaysInFirstWeek改为5天,那么很明显这一周属于2020年的天数(从周日到周一,只有 1 号到 4 号 4 天)不够 5 天,所以这一周被划归为2019年的第53周,2019-12-29到2020-01-04的week year都是属于2019。
val calendar = Calendar.getInstance()
val simpleDateFormat = SimpleDateFormat("YYYY yyyy MM dd")
calendar.set(2019, 12-1, 29)
simpleDateFormat.calendar.minimalDaysInFirstWeek = 5
println(simpleDateFormat.format(calendar.time))
//output
DATE:2019 12 29
minimalDaysInFirstWeek:5
firstDayOfWeek:1
YYYY yyyy MM dd
2019 2019 12 29再比如下面这个示例中的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?
