Groovy™ 日期和时间备忘单

作者:  Paul King
PMC 成员

发布时间:2022-10-24 07:27AM


Java 从一开始就有 Date 类,Groovy 支持使用它和一些相关的类,如 Calendar。在本博客文章中,我们将这些类称为旧版日期类。Groovy 通过更简单的机制来格式化、解析和从相关类中提取字段,从而增强了使用旧版日期类的体验。

自 Java 8 以来,JDK 中包含了 JSR-310 日期时间 API。我们将这些类称为新版日期类。新版日期类消除了旧版日期类的许多限制,并带来了许多备受赞赏的额外一致性。Groovy 也为新版日期类提供了类似的增强功能。

Groovy 对旧版日期类的增强功能位于 groovy-dateutil 模块中(在 Groovy 2.5 之前,此功能内置在核心模块中)。groovy-datetime 模块包含对新版日期类的增强功能。您可以在构建文件中包含对此模块的依赖项,或引用 groovy-all pom 依赖项。这两个模块都是标准 Groovy 安装的一部分。

接下来的几节将演示常见的日期和时间任务,以及使用新旧类和 Groovy 增强功能执行这些任务的代码。

请注意:某些格式化命令是与区域设置相关的,如果您自己运行这些示例,输出可能会略有不同

表示当前日期/时间

旧版日期类具有包含日期和时间的抽象。如果您只对其中一个方面感兴趣,您只需忽略另一个方面。新版日期类允许您拥有仅日期、仅时间和日期时间表示。

示例创建表示当前日期和/或时间的实例。从实例中提取各种信息,并以各种方式打印它们。某些示例使用 SV 宏,该宏打印一个或多个变量的名称和字符串值。

任务java.time旧版

当前日期和时间

println LocalDateTime.now()      
println Instant.now()
2022-10-24T12:40:02.218130200
2022-10-24T02:40:02.223131Z
println new Date()               
println Calendar.instance.time
Mon Oct 24 12:40:02 AEST 2022
Mon Oct 24 12:40:02 AEST 2022
当前年份中的天数 &
当前月份中的天数
println LocalDateTime.now().dayOfYear
println LocalDateTime.now().dayOfMonth
297
24
println Calendar.instance[DAY_OF_YEAR]
println Calendar.instance[DAY_OF_MONTH]
297
24
提取今天日期中的
年、月和日
var now = LocalDate.now()     // or LocalDateTime

println SV(now.year, now.monthValue, now.dayOfMonth)

(Y, M, D) = now[YEAR, MONTH_OF_YEAR, DAY_OF_MONTH]
println "Today is $Y $M $D"
now.year=2022, now.monthValue=10, now.dayOfMonth=24
Today is 2022 10 24
var now = Calendar.instance
(_E, Y, M, _WY, _WM, D) = now
println "Today is $Y ${M+1} $D"

(Y, M, D) = now[YEAR, MONTH, DAY_OF_MONTH]
println "Today is $Y ${M+1} $D"
Today is 2022 10 24
Today is 2022 10 24
打印今天的日期的替代方法
println now.format("'Today is 'YYYY-MM-dd")
printf 'Today is %1$tY-%1$tm-%1$te%n', now
Today is 2022-10-24
Today is 2022-10-24
println now.format("'Today is 'YYYY-MM-dd")
printf 'Today is %1$tY-%1$tm-%1$te%n', now
Today is 2022-10-24
Today is 2022-10-24

提取当前时间的部分

now = LocalTime.now() // or LocalDateTime
println SV(now.hour, now.minute, now.second)
(H, M, S) = now[HOUR_OF_DAY, MINUTE_OF_HOUR,
SECOND_OF_MINUTE]
printf 'The time is %02d:%02d:%02d\n', H, M, S
now.hour=12, now.minute=40, now.second=2
The time is 12:40:02
(H, M, S) = now[HOUR_OF_DAY, MINUTE, SECOND]

println SV(H, M, S)
printf 'The time is %02d:%02d:%02d%n', H, M, S
H=12, M=40, S=2
The time is 12:40:02
打印时间的替代方法
println now.format("'The time is 'HH:mm:ss")
printf 'The time is %1$tH:%1$tM:%1$tS%n', now
The time is 12:40:02
The time is 12:40:02
println now.format("'The time is 'HH:mm:ss")
printf 'The time is %1$tH:%1$tM:%1$tS%n', now
The time is 12:40:02
The time is 12:40:02

处理时间

新版日期类有一个 LocalTime 类,专门用于表示仅时间量。旧版日期类没有这种专用抽象;您基本上只是忽略日期的日、月和年部分。java.sql.Time 类可以作为替代,但很少使用。Java 将新版日期类与旧版等效类进行比较的文档,讨论了使用 GregorianCalendar 并将日期设置为纪元值 1970-01-01 作为 LocalTime 类的近似值。我们将在此处遵循该方法以提供比较,但如果您需要表示仅时间值或在 JDK 8 之前的版本上使用 Joda-Time 库,我们强烈建议升级到新类。

这些示例着眼于表示午夜前后一分钟的时间,以及您可能用餐的一些时间。对于用餐,除了打印各种值外,我们可能还对根据现有时间计算新时间感兴趣,例如午餐和晚餐相隔 7 小时。

任务java.time旧版
午夜后一分钟
LocalTime.of(0, 1).with {
println format('HH:mm')
println format('hh:mm a')
println format('K:mm a')
}
00:01
12:01 am
0:01 am
Calendar.instance.with {
clear()
set(MINUTE, 1)
println format('HH:mm')
println format('hh:mm a')
println format('K:mm a')
}
00:01
12:01 am
0:01 am
午夜前一分钟
LocalTime.of(23, 59).with {
println format('HH:mm')
println format('hh:mm a')
println format('K:mm a')
}
23:59
11:59 pm
11:59 pm
Calendar.instance.with {
clear()
set(hourOfDay: 23, minute: 59)
println format('HH:mm')
println format('hh:mm a')
println format('K:mm a')
}
23:59
11:59 pm
11:59 pm
用餐时间
var breakfast = LocalTime.of(7, 30)
var lunch = LocalTime.parse('12:30')
assert lunch == LocalTime.parse('12:30.00 pm', 'hh:mm.ss a')
lunch.with { assert hour == 12 && minute == 30 }
var dinner = lunch.plusHours(7)
assert dinner == lunch.plus(7, ChronoUnit.HOURS)
assert Duration.between(lunch, dinner).toHours() == 7
assert breakfast.isBefore(lunch) // Java API
assert lunch < dinner // Groovy shorthand
assert lunch in breakfast..dinner
assert dinner.format('hh:mm a') == '07:30 pm'
assert dinner.format('k:mm') == '19:30'
assert dinner.format(FormatStyle.MEDIUM) == '7:30:00 pm'
assert dinner.timeString == '19:30:00'
var breakfast = Date.parse('hh:mm', '07:30')
var lunch = Calendar.instance.tap {
clear()
set(hourOfDay: 12, minute: 30)
}
assert lunch[HOUR_OF_DAY, MINUTE] == [12, 30]
var dinner = lunch.clone().tap { it[HOUR_OF_DAY] += 7 }
assert dinner == lunch.copyWith(hourOfDay: 19)
assert dinner.format('hh:mm a') == '07:30 pm'
assert dinner.format('k:mm') == '19:30'
assert dinner.time.timeString == '7:30:00 pm'
assert breakfast.before(lunch.time) // Java API
assert lunch < dinner // Groovy shorthand

处理日期

要使用旧版日期类表示仅日期信息,您可以将时间方面设置为零,或者简单地忽略它们。或者,您可以考虑使用不那么常用的 java.sql.Date 类。新版日期类为此目的提供了特殊的 LocalDate 类,我们强烈推荐使用。

这些示例创建了万圣节和墨尔本杯日(澳大利亚维多利亚州的公共假日)的日期。我们查看了这两个日期的各种属性。

任务java.time旧版
假期
var halloween22 = LocalDate.of(2022, 10, 31)
var halloween23 = LocalDate.parse('2023-Oct-31', 'yyyy-LLL-dd')
assert halloween22 == halloween23 - 365
assert halloween23 == halloween22.plusYears(1)
var melbourneCup22 = LocalDate.of(2022, 11, 1)
assert halloween22 < melbourneCup22
assert melbourneCup22 - halloween22 == 1
assert Period.between(halloween22, melbourneCup22).days == 1
assert ChronoUnit.DAYS.between(halloween22, melbourneCup22) == 1L
var days = []
halloween22.upto(melbourneCup22) {days << "$it.dayOfWeek" }
assert days == ['MONDAY', 'TUESDAY']
var hols = halloween22..melbourneCup22
assert hols.size() == 2
var halloween21 = Date.parse('dd/MM/yyyy', '31/10/2021')
var halloween22 = Date.parse('yyyy-MMM-dd', '2022-Oct-31')
assert halloween21 + 365 == halloween22
var melbourneCup22 = new GregorianCalendar(2022, 10, 1).time
assert melbourneCup22.dateString == '1/11/22' // AU Locale
assert halloween22 < melbourneCup22
assert melbourneCup22 - halloween22 == 1
assert melbourneCup22 == halloween22.copyWith(month: 10, date: 1)
var days = []
halloween22.upto(melbourneCup22) { days << it.format('EEEEE') }
assert days == ['Monday', 'Tuesday']
var hols = halloween22..melbourneCup22
assert hols.size() == 2

处理日期和时间组合

新版日期类使用 LocalDateTime 来表示具有日期和时间两方面的量。前面看到的许多方法也适用于此处。

这些示例展示了如何在墨尔本杯日创建和打印午餐的表示形式。

任务java.time旧版
假期
var melbourneCupLunch = LocalDateTime.of(2022, 11, 1, 12, 30)
assert melbourneCupLunch.timeString == '12:30:00'
assert melbourneCupLunch.dateString == '2022-11-01'
assert melbourneCupLunch.dateTimeString == '2022-11-01T12:30:00'
assert melbourneCupLunch.toLocalDate() == melbourneCup22
assert melbourneCupLunch.toLocalTime() == lunch
assert melbourneCupLunch == melbourneCup22 << lunch
var melbourneCupLunch = new GregorianCalendar(2022, 10, 1, 12, 30).time
assert melbourneCupLunch.timeString == '12:30:00 pm' // Locale specific
assert melbourneCupLunch.dateString == '1/11/22' // Locale specific
assert melbourneCupLunch.dateTimeString == '1/11/22, 12:30:00 pm' // Locale specific
assert melbourneCupLunch.clearTime() == melbourneCup22

处理带时区的日期和时间

旧版日期类具有 TimeZone 的概念,主要由 Calendar 类使用。新版日期类具有类似的概念,但使用 ZoneIdZoneOffsetZonedDateTime 类(以及其他类)。

这些示例展示了时区的各种属性,并表明在墨尔本杯早餐期间,洛杉矶仍然是前一天晚上(万圣节)。它们还表明,在一年中的那个时候,这两个时区相隔 18 小时。

任务java.time旧版
假期
var aet = ZoneId.of('Australia/Sydney')
assert aet.fullName == 'Australian Eastern Time' && aet.shortName == 'AET'
assert aet.offset == ZoneOffset.of('+11:00')
var melbCupBreakfastInAU = ZonedDateTime.of(melbourneCup22, breakfast, aet)
var melbCupBreakfast = LocalDateTime.of(melbourneCup22, breakfast)
assert melbCupBreakfastInAU == melbCupBreakfast << aet
var pst = ZoneId.of('America/Los_Angeles')
assert pst.fullName == 'Pacific Time' && pst.shortName == 'GMT-08:00'
var meanwhileInLA = melbCupBreakfastInAU.withZoneSameInstant(pst)
assert halloween22 == meanwhileInLA.toLocalDate()
assert aet.offset.hours - pst.offset.hours == 18
var aet = TimeZone.getTimeZone('Australia/Sydney')
assert aet.displayName == 'Australian Eastern Standard Time'
assert aet.observesDaylightTime()
var melbourneCupBreakfast = new GregorianCalendar(aet).tap {
set(year: 2022, month: 10, date: 1, hourOfDay: 7, minute: 30)
}
var pst = TimeZone.getTimeZone('America/Los_Angeles')
assert pst.displayName == 'Pacific Standard Time'
var meanwhileInLA = new GregorianCalendar(pst).tap {
setTimeInMillis(melbourneCupBreakfast.timeInMillis)
}
assert meanwhileInLA.time.format('MMM dd', pst) == halloween22.format('MMM dd')
assert aet.rawOffset / 3600000 - pst.rawOffset / 3600000 == 18

其他有用的类

新版日期类提供了更多有用的类。以下是一些常见的类:

  • OffsetDateTime - 类似于 ZonedDateTime,但只包含与 UTC 的偏移量,而不是完整的时区

  • Instant - 类似于 OffsetDateTime,但与 UTC 绑定

  • YearMonth - 类似于 LocalDate,但没有日期组件

  • MonthDay - 类似于 LocalDate,但没有年份组件

  • Period - 用于表示时间段,例如 Period.ofDays(14)Period.ofYears(2);另请参阅上面的 LocalDate 示例。

  • Duration - 基于时间的时间量,例如 Duration.ofSeconds(30)Duration.ofHours(7);另请参阅上面的 LocalTime 示例。

转换

在新旧类之间进行转换非常有用。下面显示了一些有用的转换方法,Groovy 增强功能以蓝色显示。

来源转换方法/属性
GregorianCalendar 
toInstant()
toZonedDateTime()
from(ZonedDateTime)
Calendar
toInstant()
toZonedDateTime()
toOffsetDateTime()
toLocalDateTime()
toLocalDate()
toLocalTime()
toOffsetTime()
toDayOfWeek()
toYear()
toYearMonth()
toMonth()
toMonthDay()
zoneOffset
zoneId
Date
toInstant()
from(Instant)
toZonedDateTime()
toOffsetDateTime()
toLocalDateTime()
toLocalDate()
toLocalTime()
toOffsetTime()
toDayOfWeek()
toYear()
toYearMonth()
toMonth()
toMonthDay()
zoneOffset
zoneId
ZonedDateTime
OffsetDateTime
LocalDateTime
LocalDate
LocalTime
toDate()
toCalendar()

SimpleDateFormat 模式

我们上面看到了几个使用 formatparse 方法的例子。对于旧版日期类,许多 Groovy 增强功能委托给 SimpleDateFormat。这个类使用模式字符串表示日期/时间格式。这些是特殊字母,用于表示时间或日期组件,并与转义的字面字符串混合。特殊字母通常重复以表示数字组件的最小字段大小,以及其他组件是使用完整形式还是缩写形式。

例如,对于美国区域设置和美国太平洋时间时区,以下模式

yyyy.MM.dd G 'at' HH:mm:ss z

将适用于以下文本

2001.07.04 AD at 12:08:56 PDT
字母 描述
G纪元指定符 AD
y年份 1996; 96
Y周年份(与年份相似,但按周分配;一年的前几天/最后几天可能分配给结束/开始上周/前一周)
M年份中的月份(上下文敏感) July; Jul; 07
L年份中的月份(独立形式) July; Jul; 07
w年份中的周 27
W月份中的周 2
D年份中的天 189
d月份中的天 10
F月份中的星期几 2
E星期几的名称 Tuesday; Tue
u星期几的数字(1 = 星期一,...,7 = 星期日)
a上午/下午标记 PM
H天中的小时 (0-23) 0
k天中的小时 (1-24) 24
K上午/下午中的小时 (0-11) 0
h上午/下午中的小时 (1-12) 12
m小时中的分钟 30
s分钟中的秒 55
S毫秒 978
z时区 Pacific Standard Time; PST; GMT-08:00
Z时区 (RFC 822) -0800
X时区 (ISO 8601) -08; -0800; -08:00
'要转义文本,请在两边加单引号
''两个单引号表示一个字面单引号 '

DateTimeFormatter 模式

Groovy 对新日期类的 formatparse 增强功能委托给 DateTimeFormatter 类。其行为与我们看到的 SimpleDateFormat 类似,但转换字母略有不同:

转换后缀 描述
G纪元 AD
u年份 2004; 04
y纪元年 2004; 04
D年份中的天 189
M/L年份中的月份 7; 07; Jul; July; J
d月份中的天 10
Q/q年份中的季度 3; 03; Q3; 3rd quarter
Y基于周的年份 1996; 96
w基于周的年份中的周 27
W月份中的周 4
E星期几 Tue; Tuesday; T
e/c本地化星期几 2; 02; Tue; Tuesday; T
F月份中的周 3
a日中的上午-下午 PM
h上午-下午中的时钟小时 (1-12) 12
K上午-下午中的小时 (0-11) 0
k上午-下午中的时钟小时 (1-24) 0
H日中的小时 (0-23) 0
m小时中的分钟 30
s分钟中的秒 55
S秒中的小数 978
A日中的毫秒 1234
n秒中的纳秒 987654321
N日中的纳秒 1234000000
V时区 ID America/Los_Angeles; Z; -08:30
z时区名称 Pacific Standard Time; PST
O本地化时区偏移 GMT+8; GMT+08:00; UTC-08:00;
X零时区偏移 'Z' Z; -08; -0830; -08:30; -083015; -08:30:15;
x时区偏移 +0000; -08; -0830; -08:30; -083015; -08:30:15;
Z时区偏移 +0000; -0800; -08:00;
p填充下一个
'要转义文本,请在两边加单引号
''两个单引号表示一个字面单引号 '

本地化模式

JDK19 添加了 ofLocalizedPattern(String requestedTemplate) 方法。请求的模板是一个或多个正则表达式模式符号,按从最大到最小的单位排序,由以下模式组成:

     "G{0,5}" +        // Era
     "y*" +            // Year
     "Q{0,5}" +        // Quarter
     "M{0,5}" +        // Month
     "w*" +            // Week of Week Based Year
     "E{0,5}" +        // Day of Week
     "d{0,2}" +        // Day of Month
     "B{0,5}" +        // Period/AmPm of Day
     "[hHjC]{0,2}" +   // Hour of Day/AmPm (refer to LDML for 'j' and 'C')
     "m{0,2}" +        // Minute of Hour
     "s{0,2}" +        // Second of Minute
     "[vz]{0,4}"       // Zone

请求的模板映射到 Unicode LDML 规范中定义的可用本地化格式中最接近的格式。以下是一个用法示例:

var now = ZonedDateTime.now()
var columns = '%7s | %10s | %10s | %10s | %14s%n'
printf columns, 'locale', 'GDK', 'custom', 'local', 'both'
[locale('en', 'US'),
 locale('ro', 'RO'),
 locale('vi', 'VN')].each { locale ->
    Locale.default = locale
    var gdk = now.format('y-MM-dd')
    var custom = now.format(ofPattern('y-MM-dd'))
    var local = now.format(ofLocalizedDate(SHORT))
    var both = now.format(ofLocalizedPattern('yMM'))
    printf columns, locale, gdk, custom, local, both
}

其输出如下

locale |        GDK |     custom |      local |           both
 en_US | 2022-12-18 | 2022-12-18 |   12/18/22 |        12/2022
 ro_RO | 2022-12-18 | 2022-12-18 | 18.12.2022 |        12.2022
 vi_VN | 2022-12-18 | 2022-12-18 | 18/12/2022 | tháng 12, 2022

示例来源:此示例来自 Nicolai Parlog

格式化器格式

java.util.Formatter 类是 Java 中各种格式化的基类。它可以直接使用,也可以通过 String.formatparseprintf 或 Groovy 的 sprintf 使用。我们在上面的示例中看到了几个使用 printfparse 格式化的示例。

Formatter 类的方法将其第一个参数作为格式字符串,并接受零个或多个附加参数。格式字符串通常包含一个或多个格式说明符(以百分号开头),这些说明符表示一个格式化版本的附加参数之一应放置在字符串中的该点。格式说明符的一般形式是:

%[argument_index$][flag][width][.precision]conversion

大部分部分是可选的。argument_index 部分仅在多次(或乱序)引用附加参数之一时使用。precision 部分仅用于浮点数。flag 部分用于指示始终包含符号(+)、零填充(0)、区域设置特定的逗号分隔符(,)和左对齐(-)。width 指示输出的最小字符数。conversion 指示参数应如何处理,例如作为数字字段、日期、特殊字符或其他特殊处理。大多数转换都有大写和小写变体,对于大写变体,在转换完成后将调用 toUpperCase

转换 描述
'b', 'B'视为布尔值,如果为 null 则为 false
'h', 'H'将参数的哈希码输出为十六进制字符串
's', 'S'视为字符串
'c', 'C'视为 Unicode 字符
'd'视为十进制整数
'o'视为八进制整数
'x', 'X'视为十六进制整数
'e', 'E'视为科学记数法中的十进制数
'f'视为浮点数
'g', 'G'视为十进制或科学记数法中的浮点数
'a', 'A'视为十六进制浮点数
't', 'T'视为日期/时间转换的前缀
'%'一个字面百分号
'n'行分隔符

使用日期/时间前缀时,适用附加后缀。

用于格式化时间

转换后缀 描述
'H'24小时制的小时数,两位数字 00 - 23
'I'12小时制的小时数,两位数字 01 - 12
'k'24小时制的小时数 0 - 23
'l'12小时制的小时数 1 - 12
'M'小时内的分钟数,两位数字 00 - 59
'S'分钟内的秒数,两位数字 00 - 60
(“60”用于闰秒)
'L'秒内的毫秒数,三位数字 000 - 999
'N'秒内的纳秒数,九位数字 000000000 - 999999999
'p'区域设置特定的上午或下午标记,小写,ampm
(转换前缀 'T' 强制此输出为大写)
'z'RFC 822 样式数字时区偏移,例如 GMT -0800
(根据夏令时需要调整)
'Z'缩写时区
's'自 1970 年 1 月 1 日 00:00:00 UTC 纪元开始以来的秒数
'Q'自 1970 年 1 月 1 日 00:00:00 UTC 纪元开始以来的毫秒数

用于格式化日期

转换后缀 描述
'B'区域设置特定完整月份名称 January
'b', 'h'区域设置特定缩写月份名称 Jan
'A'区域设置特定星期几完整名称 Sunday
'a'区域设置特定星期几缩写名称 Sun
'C'四位年份的前两位数字 00 - 99
'Y'四位年份 0092
'y'年份的最后两位数字 00 - 99
'j'年份中的天数,三位数字 001 - 366
'm'月份,两位数字 01 - 13
'd'月份中的天数,两位数字 01 - 31
'e'月份中的天数 1 - 31

用于格式化日期/时间组合

转换后缀 描述
'R'时间格式化为 24 小时制,如 "%tH:%tM"
'T'时间格式化为 24 小时制,如 "%tH:%tM:%tS"
'r'时间格式化为 12 小时制,如 "%tI:%tM:%tS %Tp"
上午或下午标记('%Tp')的位置可能取决于区域设置。
'D'日期格式化为 "%tm/%td/%ty"
'F'ISO 8601 日期格式化为 "%tY-%tm-%td"
'c'日期和时间格式化为 "%ta %tb %td %tT %tZ %tY" Sun Jul 21 15:17:00 EDT 1973

更多信息