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_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
,我们看到:
//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?