在命令行中进行日期计算 - 第二部分

作者:Mitch Frazier

在本系列命令行日期计算文章的第二部分中,我们想尝试解决我们在第一部分中提到的一个问题:向 date 命令传递类似“某个日期之后的第一个星期一”的日期规范。

我解决这个问题的第一个想法是在开始日期上加 1 到 7 天,然后查看结果日期,找到与请求的星期几匹配的日期。 为了扩展解决方案以实现前一天,我们只需减去天数而不是添加天数。 为了使我们的代码更健壮,我们将使用date命令将请求的星期几转换为数字。 转换为数字会标准化我们的日期,以便“mon”、“Mon”和“Monday”都可以正常工作。 的格式字符串选项date命令可以再次帮助我们获得天数

$ date --date 'Sunday' '+%u'
7
$ date --date 'Sunday' '+%w'
0

The%u格式说明符输出一个从 1 到 7 的数字,对应于星期一到星期日。 The%w格式说明符输出一个从 0 到 6 的数字,对应于星期日到星期六。 上面的日期规范可能看起来有点奇怪,因为没有实际日期,只有星期几,在这种情况下date命令只是假设您指的是最接近的落在您指定的星期几的日期。

因此,让我们将我们的想法打包成一个函数,该函数接受三个参数:开始日期、单词“next”或“prev”以及请求的星期几。 显然,在现实世界中,您需要验证传递的参数,但我们在这里跳过它

function np_date()
{
    local date="$1"
    local dow=($(date --date "$3" '+%w')) # Convert day to number (0..6=Sun..Sat).

    if [[ $2 == 'next' ]]; then
        # Increment passed date till we get to the requested day.
        n=1
        while [[ $(date --date "$date +$n days" '+%w') != $dow ]]
        do
            let n++
        done
        date --date "$date +$n days" '+%Y-%m-%d'
    else
        # Decrement passed date till we get to the requested day.
        n=7
        while [[ $(date --date "$date -$n days" '+%w') != $dow ]]
        do
            let n--
        done
        date --date "$date -$n days" '+%Y-%m-%d'
    fi
}

每个循环之后的最后一个 date 命令通过使用确定的日期偏移量打印日期来生成函数的返回值。 请注意,我在这里使用单词“return”来表示打印到 stdout 的值,而不是函数的退出状态。

由于这是一个函数,我们需要source它(或. file它)为了测试它

$ source np_date0.sh
$ np_date 2018-09-20 next Monday
2018-09-24
$ np_date 2018-09-20 next thu
2018-09-27
$ np_date 2018-09-20 next Fri
2018-09-21
$ np_date 2018-09-20 prev Tuesday
2018-09-18
$ np_date 2018-09-20 prev wed
2018-09-19
$ np_date 2018-09-20 prev Sat
2018-09-15

这效果很好,但我必须承认,我觉得在循环中连续调用date命令有点蛮力。 诚然,最多是七次,但仍然感觉我们可以做得“更好”一点。

因此,让我们看看我们所知道的:我们的开始日期可以落在七个可能的星期几之一上,而我们请求的星期几也可以落在七个可能的星期几上。 对于开始日期的每个特定星期几,我们可以计算出每个请求的星期几的偏移量。 例如,如果开始日期是星期日 (0),则下一个星期一是提前一天,上一个星期一是落后 6 天。 如果我们计算每个可能的星期几的这些值,我们就有了一个“next”的二维矩阵

       Start
       S M T W T F S
Next
S      7 6 5 4 3 2 1
M      1 7 6 5 4 3 2
T      2 1 7 6 5 4 3
W      3 2 1 7 6 5 4
T      4 3 2 1 7 6 5
F      5 4 3 2 1 7 6
S      6 5 4 3 2 1 7

和一个“prev”的矩阵

       Start
       S M T W T F S
Prev
S      7 1 2 3 4 5 6
M      6 7 1 2 3 4 5
T      5 6 7 1 2 3 4
W      4 5 6 7 1 2 3
T      3 4 5 6 7 1 2
F      2 3 4 5 6 7 1
S      1 2 3 4 5 6 7

读取这些的方法是找到与起始日期的星期几相对应的列,然后向下读取该列以确定从它添加或减去多少天才能到达每个可能的下一个或上一个日期。

我们不能直接使用这些,因为 bash 不支持多维数组,但这在这里并不是真正的问题,因为我们可以使用我们请求的星期几来选择我们感兴趣的数组行。

因此,我们将此打包为一个函数,该函数接受两个参数:我们的 next/prev 单词和请求的星期几。 然后,我们从上述两个矩阵之一(取决于 next/prev 单词)返回与请求的星期几相对应的行

function day_offsets()
{
    local day_num=$(date --date "$2" '+%w')
    if [[ $1 == 'next' ]]; then
        local incr_d0=(7 6 5 4 3 2 1)  # Sun
        local incr_d1=(1 7 6 5 4 3 2)
        local incr_d2=(2 1 7 6 5 4 3)
        local incr_d3=(3 2 1 7 6 5 4)
        local incr_d4=(4 3 2 1 7 6 5)
        local incr_d5=(5 4 3 2 1 7 6)
        local incr_d6=(6 5 4 3 2 1 7)  # Sat
        eval echo \${incr_d${day_num}[*]}
    else
        local decr_d0=(7 1 2 3 4 5 6)  # Sun
        local decr_d1=(6 7 1 2 3 4 5)
        local decr_d2=(5 6 7 1 2 3 4)
        local decr_d3=(4 5 6 7 1 2 3)
        local decr_d4=(3 4 5 6 7 1 2)
        local decr_d5=(2 3 4 5 6 7 1)
        local decr_d6=(1 2 3 4 5 6 7)  # Sat
        eval echo \${decr_d${day_num}[*]}
    fi
}

为了返回“矩阵”中的正确行,我们可以这样做

if [[ $day_num == 0 ]]; then
    echo 7 6 5 4 3 2 1
# ...
fi

相反,我们使用eval语句来生成要返回的正确行的名称

eval echo \${incr_d${day_num}[*]}

假设请求的日期是星期日(天数 0),在替换天数后,我们最终得到

echo ${incr_d0[*]}

现在我们有了用于计算的偏移量,但我们仍然需要一个新的版本np_date函数来使用它们来计算我们的下一个或上一个日期

function np_date()
{
    local daynum=$(date --date "$1" '+%w')
    local offsets=($(day_offsets $2 $3))
    local -A operators=([next]=+ [prev]=-)

    date --date "$1 ${operators[$2]}${offsets[$daynum]} days" '+%Y-%m-%d'
}

该函数的第一行将请求的日期转换为数字。 第二行使用我们的day_offsets函数将我们的日期偏移量放入数组中。 第三行创建一个关联数组,每个可能的 next/prev 单词都有一个键。 我们使用它将 next/prev 转换为正确的运算符 (+/-) 以传递给 date 命令。 最后一行然后将所有这些部分传递给date命令以获取返回的日期值。 只是为了确保它仍然有效

$ source np_date1.sh
$ np_date 2018-09-20 next Monday
2018-09-24
$ np_date 2018-09-20 next thu
2018-09-27
$ np_date 2018-09-20 next Fri
2018-09-21
$ np_date 2018-09-20 prev Tuesday
2018-09-18
$ np_date 2018-09-20 prev wed
2018-09-19
$ np_date 2018-09-20 prev Sat
2018-09-15

我经历了更多版本的day_offsets函数,并不断努力使其更好。 第二个版本使用循环来生成数组,但我不会用它来烦你,因为结果比令人满意更令人困惑。 第三个和第四个版本基于以下观察:如果您从上述矩阵之一中取出任何一行并将其加倍,那么您可以从该结果中挑选出该矩阵的所有其余行

decr_d=(1 2 3 4 5 6 7 1 2 3 4 5 6 7)
incr_d=(7 6 5 4 3 2 1 7 6 5 4 3 2 1)

所以现在我们所要做的就是使用星期几参数来选择上面相应列表中正确的起点,然后提取接下来的七个项目。 版本 3 使用循环来输出项目,我也将跳过它,只向您展示版本 4,该版本删除了循环

function day_offsets()
{
    local day_num=$(date -d "$2" "+%w")
    if [[ $1 == 'next' ]]; then
        local incr_d=(7 6 5 4 3 2 1 7 6 5 4 3 2 1)
        local start_offset=$((day_num == 0 ? day_num + 7 : 7 - day_num))
        echo ${incr_d[@]:$start_offset:7}
    else
        local decr_d=(1 2 3 4 5 6 7 1 2 3 4 5 6 7)
        local start_offset=$((day_num == 0 ? day_num + 6 : 6 - day_num))
        echo ${decr_d[@]:$start_offset:7}
    fi
}

特别注意 echo 行(例如echo ${xxx_incr[@]:$start_offset:7})。 我们没有从起点开始循环,而是使用:start-expr:length-expr语法(在我们的例子中是:$start_offset:7)来获取我们想要的七个项目。 诚然,我直到最近才意识到这种能力,我以前曾使用这种语法从变量中提取子字符串,但从未提取数组切片。 如果您也错过了它,请不要感到难过,问题在于它没有记录在手册页的数组部分中,而是记录在参数扩展部分中,其中:start-expr:length-expr语法被描述。

让我们再测试一次

$ DAY_OFFSETS=3 source np_date1.sh
$ np_date 2018-09-20 next Monday
2018-09-24
$ np_date 2018-09-20 next thu
2018-09-27
$ np_date 2018-09-20 next Fri
2018-09-21
$ np_date 2018-09-20 prev Tuesday
2018-09-18
$ np_date 2018-09-20 prev wed
2018-09-19
$ np_date 2018-09-20 prev Sat
2018-09-15

如果您想知道那个DAY_OFFSETS=3是怎么回事,请记住有四个版本的day_offsets函数,在名为day_offsetsN.sh(N 从 0 到 3)。 的day_offsets.sh脚本实际上只是一个简短的脚本,它根据DAY_OFFSETS变量的值(如果未设置变量,则选择版本零)来包含相应的版本

$ cat day_offsets.sh
eval source day_offsets${DAY_OFFSETS:-0}.sh

我们使用默认值变量替换语法 (${var:-default})来获取DAY_OFFSETS的值,然后将其与我们的老朋友eval一起使用,而不是使用 if 语句,来 source 相应的文件(再次忽略错误检查)。

那么,我们是否成功地使其比我们最初的蛮力版本更好? 我们确实在尝试中使用了一些有趣的 bash 结构eval、关联数组和数组切片,但在全面考虑后,我不确定我们是否使其变得更好。

Mitch Frazier 是 Emerson Electric Co. 的嵌入式系统程序员。自 2000 年代初以来,Mitch 一直是Linux Journal的贡献者和朋友。

加载 Disqus 评论