如何用cal命令查找闰年(附代码)

201 阅读5分钟

你知道macOS和Linux的命令行里有一个日历吗?有的!

卡尔简介

$ cal

     July 2021
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

快速检查一下tldr ,就会发现这个工具是多么的有用

$ tldr cal

cal

Prints calendar information.

- Display a calendar for the current month:
    cal

- Display previous, current and next month:
    cal -3

- Display a calendar for a specific month (1-12 or name):
    cal -m month

- Display a calendar for the current year:
    cal -y

- Display a calendar for a specific year (4 digits):
    cal year

- Display a calendar for a specific month and year:
    cal month year

- Display date of Easter (Western Christian churches) in a given year:
    ncal -e year

但有一点是它没有的。闰年!

Cal当然知道闰年,否则它就不是一个好的日历了。但它没有办法列出它们。

识别闰年,黑客式的

让我们自己来建立它吧!当然,我们可以使用带有日历库的真正的编程语言,但是通过组装命令行组件来构建东西是很有趣的,而且我们又不打算将这种计算方式投入生产。

首先,让我们同意二月是闰年的指标:

$ cal 02 2021

   February 2021
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

$ cal 02 2020

   February 2020
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

我们可以搜索 "29",让它识别闰年吗?也许可以!"。

$ cal 02 2020 | grep -q 29 && echo "LEAP" || echo "normal"

LEAP

$ cal 02 2021 | grep -q 29 && echo "LEAP" || echo "normal"

normal

到目前为止还不错?但我打赌你看到了问题所在:

$ for year in {2020..2030}; do
  printf "$year ";
  cal 02 "$year" | grep -q 29 && echo "LEAP" || echo "year";
done

2020 LEAP
2021 year
2022 year
2023 year
2024 LEAP
2025 year
2026 year
2027 year
2028 LEAP
2029 LEAP
2030 year

2029年不是一个闰年。它的匹配只是因为2029年的29。

$ cal 02 2029

   February 2029
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

我们可以通过删除整行的 "February "来消除这个麻烦的结果:

$ cal 02 2029 | grep -v Feb

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",那么它确实是一个闰年:

$ for year in {2020..2030}; do
  printf "$year ";
  cal 02 "$year" | grep -v Feb | grep -q 29 && echo "LEAP" || echo "year";
done

2020 LEAP
2021 year
2022 year
2023 year
2024 LEAP
2025 year
2026 year
2027 year
2028 LEAP
2029 year

这与实际的闰年相吻合吗?

Wolfram Research的 "闰年"页面中列出了21世纪上半叶的闰年:

2000
2004
2008
2012
2016
2020
2024
2028
2032
2036
2040
2044
2048

让我们看看我们如何做

$ for year in {2000..2051}; do cal 02 "$year" | grep -v Feb | grep -q 29 && echo $year; done
2000
2004
2008
2012
2016
2020
2024
2028
2032
2036
2040
2044
2048

完成了!

但是这种搜索29的方法太简单了。让我们变得更怪异吧!

识别闰年,甚至更多的黑客行为

闰年和正常年份的区别在于多出了29日这一天。我们能不能检查一下cal 输出中的字符数来识别闰年?

是的,我们可以。但是有一个问题要先解决。让我告诉你我的意思。

我们知道2020年是闰年,2021年不是闰年。让我们检查一下这两个年份的2月所输出的字符数的差异。我们将用方便的wc 工具来做,特别是wc -c 来计算字节字符:

$ printf "hello" | wc -c

5

"hello "中有5个字符(使用printf ,而不是echo ,因为否则我们也会有一个换行字符)。很好!

$ cal 02 2020 | wc -c

184
$ cal 02 2021 | wc -c

184

绊脚石来了。每个月都有不同的输出(2020年有第29天),但它们都有相同的字符数,因为cal增加了空格以确保格式一致。空格和数字一样,都是可计算的字符。

我们能做什么呢?

我们可以删除所有的空格!这样就只剩下我们想要计算的字符了。

删除字符的一个很好的工具是tr 命令。正如我们在Let's write a shell script中看到的,tr 命令可以翻译字符串数据。例如,它可以将所有 "a "字符改为 "A "字符。它还可以删除所有指定的字符,这正是我们想要的。

在我们的例子中,我们想删除所有的空白字符,不管它们是什么(空格、制表符、换行符等等)。有一个有用的分组,称为字符类,用于此。[:space:] ,将针对所有这些许多空白字符。

$ cal 02 2020 | tr -d '[:space:]'

February2020SuMoTuWeThFrSa1234567891011121314151617181920212223242526272829
$ cal 02 2021 | tr -d '[:space:]'

February2021SuMoTuWeThFrSa12345678910111213141516171819202122232425262728

很好!我们现在可以计算字符的数量来寻找闰年吗?我们当然可以!

$ cal 02 2020 | tr -d '[:space:]' | wc -c

75
$ cal 02 2021 | tr -d '[:space:]' | wc -c

73

现在让我们输出一些闰年吧!

$ for year in {2000..2051}; do
  cal 02 "$year" |
    tr -d '[:space:]' |
    wc -c |
    grep -q 75 && echo "$year";
done

2000
2004
2008
2012
2016
2020
2024
2028
2032
2036
2040
2044
2048

在黑客上的黑客,它是有效的!

你认为哪个更快?是的,我也不知道。 "删除二月 "的路径可能更快?但如果有很大的区别,我会很惊讶。

让我们不要惊讶。让我们来看看!

有一个很好的工具叫hyperfine ,可以评估多个命令行调用的性能

哪个更快?Hyperfine会告诉我们

这是我给hyperfine的标志,以及两个命令:

--style basic
    Plain output styling
--export-markdown hyperfine.md
    Export results as markdown
--warmup 5
     Do five runs before benchmarking
--ignore-failure
     Ignore non-zero exits

我们真正关心的不是年份循环本身的基准,而是年份的计算,所以我只对通过每个转换解析一个年份进行基准测试。

$ hyperfine --style basic --export-markdown hyperfine.md --warmup 5 --ignore-failure "cal 02 2050 | grep -v Feb | grep -q 29" "cal 02 2050 | tr -d '[:space:]' | wc -c | grep -q 75"

Benchmark #1: cal 02 2050 | grep -v Feb | grep -q 29
  Time (mean ± σ):       1.8 ms ±   0.3 ms    [User: 0.7 ms, System: 1.9 ms]
  Range (min … max):     1.0 ms3.6 ms    493 runs

  Warning: Command took less than 5 ms to complete. Results might be inaccurate.
  Warning: Ignoring non-zero exit code.

Benchmark #2: cal 02 2050 | tr -d '[:space:]' | wc -c | grep -q 75
  Time (mean ± σ):       1.9 ms ±   0.4 ms    [User: 0.9 ms, System: 2.7 ms]
  Range (min … max):     0.7 ms5.9 ms    811 runs

  Warning: Command took less than 5 ms to complete. Results might be inaccurate.
  Warning: Ignoring non-zero exit code.

Summary
  'cal 02 2050 | grep -v Feb | grep -q 29' ran
    1.07 ± 0.28 times faster than 'cal 02 2050 | tr -d '[:space:]' | wc -c | grep -q 75'
命令平均[毫秒]最小[ms]最大[毫秒]相对数
cal 02 2050 | grep -v Feb | grep -q 291.8 ± 0.31.03.61.00
cal 02 2050 | tr -d '[:space:]' | wc -c | grep -q 751.9 ± 0.40.75.91.07 ± 0.28

正如预期的那样,就挂钟时间而言,它们之间几乎没有区别,但 "remove February "路径可能稍快一些。

至此,这个命令行工具组装之旅结束了。

总结

  • 你可以通过将现有的工具组装成一个数据管道来建立新的命令行功能
  • 有点创造力可以从简单的部分获得令人惊讶的复杂结果
  • Hyperfine是一个有用的工具,可以对命令行命令进行基准测试。

接下来的步骤

  • 乱用cal 命令。
  • 你能建立一个管道来计算某一年的所有非空格字符吗?cal 2020
  • 试着针对1700-1799年运行该管道:注意到有什么奇怪的吗?