计算星期几
对于那些在家中一起学习的读者,你们可能还记得我们这位勇敢的主角正在编写一个 shell 脚本,它可以告诉你特定日期在星期几出现的最近年份——例如,圣诞节在星期四出现的最近年份。
通常情况下,有一些细微差别和边缘情况使得这种计算有点棘手,包括需要识别指定日期是否已在当年过去,因为如果现在是七月,而我们正在搜索最近的星期日 5 月 1 日,如果我们只是从前一年开始搜索,我们就会错过 2011 年。
事实上,正如任何软件开发人员所知,程序的核心逻辑通常很容易组装。真正让编程成为一项注重细节的挑战的是所有那些讨厌的角落情况,那些奇怪的、不太可能出现的情况,程序需要识别并正确响应。这可能很有趣,但也可能令人筋疲力尽,并且需要数周的调试才能确保出色的覆盖率。
我们的这个脚本也遇到了同样的情况。在每月第一天是星期日的月份,我们已经准备就绪。给我一个数字日期,我可以很快告诉你它是星期几。不幸的是,这只是可能月份配置的 1/7。
那个月份中的日是星期几?为了便于讨论,让我们介绍两个首字母缩略词:DOM 是月份中的日 (Day Of Month),DOW 是星期几 (Day Of Week)。2011 年 5 月 3 日的 DOM=3,DOW=3,因为它是星期二。
cal 实用程序显示本月如下所示
May 2011
Su Mo Tu We Th Fr Sa
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
看!一个完美形成的月份,所以很容易计算出某天的星期几。但是,这对我们的测试来说不太公平,所以让我们向前移动一个月到六月,看看 6 月 3 日。那是 DOM=3,DOW=6(星期五)
June 2011
Su Mo Tu We Th Fr Sa
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
我将要使用的解决方案可能比必要的更复杂,但它是我的,我会坚持使用它。
这是想法。当 awk 遍历行时,它可以很容易地确定 NF(字段数)。如果 NF < 7,我们有一个月份,其中第一天从星期日以外的星期几开始。例如,2011 年 6 月第一周的任何匹配日期,NF = 4。
回顾一下六月,因为它很重要,要认识到该月的最后一周也有问题。它的 NF=5。但是,由于该行中的任何匹配项的 DOM 必须 > 7,因此我们可以稍后解决这个细微差别。正如他们所说,敬请期待。
然而,考虑到所有这些信息,并且 i 是月份中的日,我们可以用来计算一个月的第一周的星期几的公式是 DOW=i+(7-NF)。一些测试用例验证了它有效
June 3 = i=3, NF=4 DOW=(7-4)+3 = 6
July 1 = i=1, NF=2 DOW=(7-2)+1 = 6
May 2 = i=2, NF=7 DOW=(7-7+2 = 2
然而,对于任何不在第一周发生的日期,我们可以忽略所有这些复杂的计算,只需获取星期几即可。
你如何判断它是否在第一周?另一个测试。搜索匹配的 DOM,然后查看匹配的行号。如果它不是第 1 行,我们必须从匹配的 cal 输出行计算星期几
awk "/$expr/ { for (i=1;i<=NF;i++)
{ if (\$i~/${day}/) { print i }}}"
在我之前的专栏中,我创建了这个过于复杂的正则表达式来匹配所有边缘情况(字面意思是,匹配是某周的第一天或最后一天的那些情况)。相反,这里有一个更快且不那么复杂的新计划。我们将使用 sed 在每个日历中填充前导和尾随空格
cal june 2011 | sed 's/^/ /;s/$/ /'
现在我们的正则表达式可以轻松地匹配指定的日期,而不是其他日期
[^0-9]DATEINQUESTION[^0-9]
此外,awk 也很容易为我们提供 NF 值,所以这里是一个给定月份中的日、月份和年份的 DOW 函数的粗略骨架
figureDOM()
{
day=$1; caldate="$2 $3"
expr="[^0-9]${day}[^0-9]"
NFval=$(cal $caldate | sed 's/^/ /;s/$/ /' | \
awk "/$expr/ { print NF }")
DOW="$(( $day + ( 7 - $NFval ) ))"
}
如果我们只搜索在当月第一周的匹配项,这就可以工作,但这当然是不现实的,所以这里有一个更好、更健壮的脚本
figureDOW()
{
day=$1; caldate="$2 $3"
expr="[^0-9]${day}[^0-9]"
cal $caldate | sed 's/^/ /;s/$/ /' > $temp
NRval=$(cat $temp | awk "/$expr/ { print NR }")
NFval=$(cat $temp | awk "/$expr/ { print NF }")
if [ $NRval -eq 3 ] ; then
DOW="$(( $day + ( 7 - $NFval ) ))"
else
DOW=$(cat $temp | awk "/$expr/
{ for (i=1;i<=NF;i++) { if (\$i~/${day}/) { print i }}}")
fi
/bin/rm -f $temp
}
一些快速测试
DOW of 3 june 2011 = 6
DOW of 1 july 2011 = 6
DOW of 2 may 2011 = 2
DOW of 16 may 2011 = 2
看起来不错!
下次,我们将把这一切联系起来。我们有一个函数可以计算给定日期的星期几,我们已经弄清楚如何解析用户输入以获取指定月份/日期对的所需星期几,并且我们知道如何确定我们向后日期搜索的起点是否是当年(例如,我们是否已经过了当年中的那个点)。
日历图片来自 Shutterstock.com。