计算星期几

作者:Dave Taylor

对于那些在家中一起学习的读者,你们可能还记得我们这位勇敢的主角正在编写一个 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。

Dave Taylor 在 UNIX 和 Linux 系统上编写 shell 脚本已经很长时间了。他是 Learning Unix for Mac OS XWicked Cool Shell Scripts 的作者。您可以在 Twitter 上通过 @DaveTaylor 找到他,您也可以通过他的技术问答网站联系他:Ask Dave Taylor

加载 Disqus 评论