Dieser Beitrag erschien ursprünglich auf Stephens Website und wird hier (leicht überarbeitet) mit Genehmigung erneut veröffentlicht
Einführung in cal
Wusstest du, dass es in der Kommandozeile von macOS und Linux einen Kalender gibt?
$ 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
Ein kurzer Blick in tldr zeigt, wie nützlich dieses Werkzeug sein kann
$ 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
Aber eines hat es nicht. Schaltjahre!
Cal kennt Schaltjahre natürlich, sonst wäre es kaum ein Kalender. Aber es gibt keine Möglichkeit, sie aufzulisten.
Schaltjahre erkennen, auf die hackige Art
Bauen wir es selbst! Klar, wir könnten eine echte Programmiersprache mit einer Kalenderbibliothek verwenden, aber Dinge zu bauen, indem man Kommandozeilen-Bausteine zusammensetzt, macht Spaß, und wir werden diese Berechnung ja nicht in Produktion bringen.
Zuerst einmal halten wir fest, dass der Februar der Indikator für ein Schaltjahr ist.
$ 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
Könnten wir nach „29“ suchen und damit Schaltjahre erkennen? Vielleicht!
$ cal 02 2020 | grep -q 29 && echo "LEAP" || echo "normal"
LEAP
$ cal 02 2021 | grep -q 29 && echo "LEAP" || echo "normal"
normal
So weit, so gut? Aber ich wette, du siehst das Problem.
$ 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 ist KEIN Schaltjahr. Es passt nur wegen der 29 in 2029.
$ 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
Wir könnten dieses lästige Ergebnis loswerden, indem wir die ganze Zeile mit „February“ außer Acht lassen.
$ 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
Jetzt behaupte ich: Wenn wir eine „29“ im Ergebnis finden, dann ist es WIRKLICH ein Schaltjahr.
$ 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
Stimmt das mit den tatsächlichen Schaltjahren überein?
Die Seite Leap Year von Wolfram Research enthält diese Liste der Schaltjahre in der ersten Hälfte des 21. Jahrhunderts.
2000
2004
2008
2012
2016
2020
2024
2028
2032
2036
2040
2044
2048
Schauen wir, wie wir abschneiden
$ 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
Fertig!
Aber dieses Grep nach der 29 ist zu einfach. Werden wir schräger!
Schaltjahre erkennen, noch hackiger
Der Unterschied zwischen einem Schaltjahr und einem normalen Jahr ist dieser zusätzliche Tag, der 29. Könnten wir die Anzahl der Zeichen in der Ausgabe von cal prüfen, um Schaltjahre zu erkennen?
Ja, können wir. Aber zuerst gibt es eine Stolperfalle. Ich zeige dir, was ich meine.
Wir wissen, dass 2020 ein Schaltjahr ist und 2021 keines. Schauen wir uns den Unterschied zwischen der Anzahl der ausgegebenen Zeichen für den Februar dieser beiden Jahre an. Das machen wir mit dem praktischen Werkzeug wc, genauer mit wc -c, um die Byte-Zeichen zu zählen.
$ printf "hello" | wc -c
5
5 Zeichen in „hello“ (mit printf statt echo, weil wir sonst noch ein Zeilenumbruchzeichen hätten). Super!
$ cal 02 2020 | wc -c
184
$ cal 02 2021 | wc -c
184
Da ist der Stolperstein. Jeder Monat hat eine andere Ausgabe (2020 hat diesen 29. Tag), aber beide haben dieselbe Anzahl an Zeichen, weil cal Leerzeichen einfügt, um eine einheitliche Formatierung zu gewährleisten. Leerzeichen sind genauso zählbare Zeichen wie Ziffern.
Was können wir tun?
Wir können alle Leerzeichen entfernen! Dann bleiben nur die Zeichen übrig, die wir zählen wollen.
Ein großartiges Werkzeug zum Entfernen von Zeichen ist der Befehl tr. Der Befehl tr übersetzt String-Daten. Zum Beispiel kann er alle a-Zeichen in A-Zeichen umwandeln.
$ echo "aardvark" | tr a A
AArdvArk
Er kann auch alle angegebenen Zeichen löschen, was genau das ist, was wir wollen.
In unserem Fall wollen wir alle Whitespace-Zeichen löschen, egal welche es sind (Leerzeichen, Tabs, Zeilenumbrüche usw.). Dafür gibt es eine praktische Gruppierung namens Zeichenklasse, mit der genau das möglich ist. Die Zeichenklasse [:space:] erfasst all diese verschiedenen Arten von Whitespace-Zeichen.
$ cal 02 2020 | tr -d '[:space:]'
February2020SuMoTuWeThFrSa1234567891011121314151617181920212223242526272829
$ cal 02 2021 | tr -d '[:space:]'
February2021SuMoTuWeThFrSa12345678910111213141516171819202122232425262728
Super! Können wir jetzt die Anzahl der Zeichen zählen, um Schaltjahre zu finden? Aber klar doch!
$ cal 02 2020 | tr -d '[:space:]' | wc -c
75
$ cal 02 2021 | tr -d '[:space:]' | wc -c
73
Jetzt geben wir ein paar Schaltjahre aus!
$ 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
Hack auf Hack, und es funktioniert!
Was glaubst du, ist schneller? Ja, ich habe auch keine Ahnung. Der Weg über das Entfernen des Februars ist vielleicht schneller? Aber ich wäre überrascht, wenn es einen großen Unterschied gäbe.
Überraschen wir uns lieber nicht. Finden wir es heraus!
Es gibt ein großartiges Werkzeug namens hyperfine, mit dem man die Leistung mehrerer Kommandozeilenaufrufe bewerten kann
Was ist schneller? Hyperfine verrät es uns
Hier sind die Flags, die ich hyperfine zusammen mit den beiden Befehlen mitgebe.
--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
Uns interessiert nur das Benchmarking der Berechnung eines Jahres, nicht die Zeit, die zum Erzeugen der Jahresschleife nötig ist. Also benchmarke ich nur die Schaltjahrprüfung für ein bestimmtes Jahr mit jedem Ansatz.
$ 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 ms … 3.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 ms … 5.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'
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
cal 02 2050 | grep -v Feb | grep -q 29 |
1.8 ± 0.3 | 1.0 | 3.6 | 1.00 |
cal 02 2050 | tr -d '[:space:]' | wc -c | grep -q 75 |
1.9 ± 0.4 | 0.7 | 5.9 | 1.07 ± 0.28 |
Wie erwartet gibt es praktisch keinen Unterschied bei der Laufzeit, ABER der Weg über das Entfernen des Februars ist möglicherweise etwas schneller.
Damit endet diese Reise durch das Zusammensetzen von Kommandozeilen-Tools.
Fazit
- Du kannst neue Kommandozeilenfunktionen bauen, indem du vorhandene Tools zu einer Datenpipeline zusammensetzt
- Ein bisschen Kreativität kann aus einfachen Bausteinen überraschend komplexe Ergebnisse holen
- Hyperfine ist ein nützliches Tool zum Benchmarken von Kommandozeilenbefehlen
Nächste Schritte
- Beschäftige dich mit dem Befehl
cal. - Versuch, eine Pipeline zu bauen, die alle Nicht-Leerzeichen eines ganzen Jahres zählt:
cal 2020 - Versuch, diese Pipeline zur Zählung eines ganzen Jahres für jedes Jahr von 1750 bis 1759 auszuführen: Fällt dir etwas Merkwürdiges auf?