在 Groovy 中计算日期和时间范围

使用 Groovy 日期和时间来发现和显示时间增量。
52 位读者喜欢这篇文章。
clock

CC BY-SA Opensource.com

我时不时需要进行一些与日期相关的计算。几天前,一位同事要求我在我们的(当然是开源的!)项目管理系统中设置一个新的项目定义。这个项目将于 8 月 1 日开始,12 月 31 日结束。提供的服务预算为每周 10 小时。

所以,是的,我必须弄清楚从 2021-08-01 到 2021-12-31(包括这两天)之间有多少周。

这是使用一个小的 Groovy 脚本解决这类问题的完美方式。

在 Linux 上安装 Groovy

Groovy 基于 Java,因此需要安装 Java。Linux 发行版的仓库中可能同时包含 Java 和 Groovy 的最新且不错的版本。或者,您可以按照 groovy-lang.org 上的说明安装 Groovy。

对于 Linux 用户来说,SDKMan 是一个不错的选择,它可以用来获取 Java、Groovy 和许多其他相关工具的多个版本。在本文中,我使用的是我的发行版的 OpenJDK11 版本和 SDKMan 的最新 Groovy 版本。

使用 Groovy 解决问题

自 Java 8 以来,时间和日期计算已被整合到一个名为 java.time 的新包中,而 Groovy 提供了对该包的访问。这是脚本:

import java.time.*

import java.time.temporal.*

def start = LocalDate.parse('2021-08-01','yyyy-MM-dd')

def end = LocalDate.parse('2022-01-01','yyyy-MM-dd')

println "${ChronoUnit.WEEKS.between(start,end)} weeks between $start and $end"

将此代码复制到一个名为 wb.groovy 的文件中,并在命令行中运行它以查看结果

$ groovy wb.groovy
21 weeks between 2021-08-01 and 2022-01-01

让我们回顾一下发生了什么。

日期和时间

java.time.LocalDate 提供了许多有用的静态方法(例如上面显示的 parse(),它允许我们根据模式将字符串转换为 LocalDate 实例,在本例中为 ‘yyyy-MM-dd’)。格式字符在很多地方都有解释——例如,java.time.format.DateTimeFormat 的文档。请注意,M 代表“月份”,而不是 m,后者代表“分钟”。因此,此模式定义了一个日期格式,为四位数的年份,后跟一个连字符,后跟一个两位数的月份数字 (1-12),再后跟一个连字符,再后跟一个两位数的月份中的日数字 (1-31)。

另请注意,在 Java 中,parse() 需要 DateTimeFormat 的实例

parse(CharSequence text, DateTimeFormatter formatter)

因此,解析变成了一个两步操作,而 Groovy 提供了 parse() 的附加版本,该版本直接接受格式字符串来代替 DateTimeFormat 实例。

java.time.temporal.ChronoUnit,实际上是一个 Enum,提供了几个 Enum 常量,例如 WEEKS(或 DAYS,或 CENTURIES...),它们反过来提供了 between() 方法,该方法允许我们计算两个 LocalDate(或其他类似的日期或时间数据类型)之间这些单位的间隔。请注意,我使用 2022 年 1 月 1 日作为 end 的值;这是因为 between() 跨越的时间段从给定的第一个日期开始,一直到但不包括给定的第二个日期。

更多日期运算

我时不时需要知道在特定时间范围内(例如,一个月)有多少个工作日。这个方便的脚本将为我计算出来

import java.time.*

def holidaySet = [LocalDate.parse('2021-01-01'), LocalDate.parse('2021-04-02'),
    LocalDate.parse('2021-04-03'), LocalDate.parse('2021-05-01'),
    LocalDate.parse('2021-05-15'), LocalDate.parse('2021-05-16'),
    LocalDate.parse('2021-05-21'), LocalDate.parse('2021-06-13'),
    LocalDate.parse('2021-06-21'), LocalDate.parse('2021-06-28'),
    LocalDate.parse('2021-06-16'), LocalDate.parse('2021-06-18'),
    LocalDate.parse('2021-08-15'), LocalDate.parse('2021-09-17'),
    LocalDate.parse('2021-09-18'), LocalDate.parse('2021-09-19'),
    LocalDate.parse('2021-10-11'), LocalDate.parse('2021-10-31'),
    LocalDate.parse('2021-11-01'), LocalDate.parse('2021-11-21'),
    LocalDate.parse('2021-12-08'), LocalDate.parse('2021-12-19'),
    LocalDate.parse('2021-12-25')] as Set

def weekendDaySet = [DayOfWeek.SATURDAY,DayOfWeek.SUNDAY] as Set

int calcWorkingDays(start, end, holidaySet, weekendDaySet) {
    (start..<end).inject(0) { subtotal, d ->
        if (!(d in holidaySet || DayOfWeek.from(d) in weekendDaySet))
            subtotal + 1
        else
            subtotal
    }
}

def start = LocalDate.parse('2021-08-01')
def end = LocalDate.parse('2021-09-01')

println "${calcWorkingDays(start,end,holidaySet,weekendDaySet)} working day(s) between $start and $end"

将此代码复制到一个名为 wdb.groovy 的文件中,并在命令行中运行它以查看结果

$ groovy wdb.groovy
22 working day(s) between 2021-08-01 and 2021-09-01

让我们回顾一下这个。

首先,我创建了一个节假日日期集合(如果您想知道,这些是智利 2021 年的“días feriados”),名为 holidaySet。请注意,LocalDate.parse() 的默认模式是 ‘yyyy-MM-dd’,所以我在这里省略了模式。另请注意,我正在使用 Groovy 简写 [a,b,c] 来创建一个 List,然后将其强制转换为 Set

接下来,我想跳过星期六和星期日,所以我创建了另一个集合,其中包含 java.time.DayOfWeek 的两个 enum 值——SATURDAYSUNDAY

然后我定义了一个方法 calcWorkingDays(),它接受开始日期、结束日期(根据前面的 between() 示例,它是我想考虑的范围之外的第一个值)、节假日集合和周末日集合作为参数。逐行来看,此方法:

  • 定义 startend 之间的范围,在 end 上是开放的(这就是 <end 的意思),并在范围内的连续元素 d 上执行 传递给 inject() 方法的闭包参数(inject() 在 Groovy 中实现 List 上的 'reduce' 操作)
    • 只要 d 既不在 holidaySet 中也不在 weekendDaySet 中,就将 subtotal 递增 1
  • 返回 inject() 返回的结果值

接下来,我定义了我想计算工作日的 startend 日期。

最后,我使用 Groovy GString 调用 println 来评估 calcWorkingDays() 方法并显示结果。

请注意,我可以使用 each 闭包而不是 inject,甚至可以使用 for 循环。我也可以使用 Java Streams 而不是 Groovy 范围、列表和闭包。有很多选择。

但是为什么不使用 groovy.Date 呢?

你们中的一些 Groovy 老用户可能想知道为什么我不使用经典的 groovy.Date。答案是,我可以使用它。但是 Groovy Date 基于 Java Date,并且有一些充分的理由转向 java.time,即使 Groovy Date 为 Java Date 添加了很多不错的功能。

对我来说,主要原因是 Java Date 的实现中存在一些不太好的设计决策,最糟糕的是它是没有必要的可变的。我花了一段时间追踪一个奇怪的错误,这个错误源于我对 Groovy Date 上的 clearTime() 方法的错误理解。我了解到它实际上清除了日期实例的时间字段,而不是返回时间部分设置为“00:00:00”的日期值。

Date 实例也不是线程安全的,这对于多线程应用程序来说可能有点挑战性。

最后,将日期和时间都包装在一个字段中并不总是方便的,并且可能导致一些奇怪的数据建模扭曲。例如,考虑一下一天中发生多个事件的情况:理想情况下,date 字段应该在日期上,而 time 字段应该在每个事件上;但这对于 Groovy Date 来说并不容易做到。

Groovy 很棒

Groovy 是一个 Apache 项目,它为 Java 提供了简化的语法,因此您可以将其用于快速简单的脚本以及复杂的应用程序。您保留了 Java 的强大功能,但您可以使用高效的工具集来访问它。 立即尝试,看看您是否能找到使用 Groovy 的乐趣。

接下来阅读什么
标签
Chris Hermansen portrait Temuco Chile
自从 1978 年毕业于不列颠哥伦比亚大学以来,我几乎没有离开过计算机,自 2005 年以来一直是全职 Linux 用户,1986 年至 2005 年是全职 Solaris 和 SunOS 用户,在此之前是 UNIX System V 用户。

评论已关闭。

© . All rights reserved.