Cet article est initialement paru sur le site de Stephen et est republié ici (légèrement remanié) avec son autorisation
Introduction à cal
Savais-tu qu'il existe un calendrier en ligne de commande sous macOS et 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
Un rapide coup d'œil à tldr montre à quel point cet utilitaire peut être utile
$ 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
Mais il lui manque une chose. Les années bissextiles !
Bien sûr, cal connaît les années bissextiles, sinon ce ne serait pas vraiment un calendrier. Mais il n'a aucun moyen de les lister.
Reconnais les années bissextiles, à la bidouille
Construisons-le nous-mêmes ! On pourrait bien sûr utiliser un vrai langage de programmation avec une bibliothèque de calendrier, mais construire des choses en assemblant des composants en ligne de commande, c'est amusant, et ce n'est pas comme si on allait mettre ce calcul en production.
Pour commencer, convenons que le mois de février est l'indicateur d'une année bissextile.
$ 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
Pourrait-on chercher « 29 » et lui faire reconnaître les années bissextiles ? Peut-être !
$ cal 02 2020 | grep -q 29 && echo "LEAP" || echo "normal"
LEAP
$ cal 02 2021 | grep -q 29 && echo "LEAP" || echo "normal"
normal
Jusque-là, tout va bien ? Mais je parie que tu vois le problème.
$ 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 n'est PAS une année bissextile. Le résultat ne correspond que parce que 2029 contient un 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
On pourrait éliminer ce résultat gênant en écartant de l'analyse toute la ligne contenant « 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
Maintenant, je soutiens que si on trouve un « 29 » dans le résultat, alors c'est VRAIMENT une année bissextile.
$ 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
Est-ce que cela correspond aux vraies années bissextiles ?
La page Année bissextile de Wolfram Research donne cette liste des années bissextiles de la première moitié du XXIe siècle.
2000
2004
2008
2012
2016
2020
2024
2028
2032
2036
2040
2044
2048
Voyons ce que ça donne
$ 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
C'est fait !
Mais ce grep sur 29 est trop facile. Passons à quelque chose de plus tordu !
Reconnais les années bissextiles, encore plus à la bidouille
La différence entre une année bissextile et une année normale, c'est ce jour supplémentaire du 29. Pourrait-on vérifier le nombre de caractères de la sortie de cal pour reconnaître les années bissextiles ?
Oui, on peut. Mais il y a d'abord un écueil à franchir. Laisse-moi te montrer ce que je veux dire.
On sait que 2020 est une année bissextile et que 2021 n'en est pas une. Comparons le nombre de caractères produits pour le mois de février de chacune de ces deux années. Pour cela, on va utiliser le pratique utilitaire wc, plus précisément wc -c pour compter les caractères en octets.
$ printf "hello" | wc -c
5
5 caractères dans « hello » (on utilise printf plutôt que echo, sinon on aurait en plus un caractère de nouvelle ligne). Parfait !
$ cal 02 2020 | wc -c
184
$ cal 02 2021 | wc -c
184
Voilà l'obstacle. Chaque mois produit une sortie différente (2020 a ce 29e jour), mais les deux ont le même nombre de caractères, car cal ajoute des espaces pour garantir une mise en forme régulière. Les espaces sont des caractères que l'on compte au même titre que les chiffres.
Alors, que faire ?
On peut supprimer tous les espaces ! Il ne resterait alors que les caractères que l'on veut compter.
Un excellent utilitaire pour supprimer des caractères est la commande tr. La commande tr transforme des données de type string. Par exemple, elle peut remplacer tous les caractères a par des caractères A.
$ echo "aardvark" | tr a A
AArdvArk
Elle peut aussi supprimer tous les caractères spécifiés, ce qui est exactement ce que l'on veut.
Dans notre cas, on veut supprimer tous les caractères d'espacement, quels qu'ils soient (espaces, tabulations, sauts de ligne, etc.). Il existe un regroupement pratique appelé classe de caractères qui permet de faire exactement cela. La classe de caractères [:space:] cible tous ces différents types de caractères d'espacement.
$ cal 02 2020 | tr -d '[:space:]'
February2020SuMoTuWeThFrSa1234567891011121314151617181920212223242526272829
$ cal 02 2021 | tr -d '[:space:]'
February2021SuMoTuWeThFrSa12345678910111213141516171819202122232425262728
Parfait ! Peut-on maintenant compter le nombre de caractères pour trouver les années bissextiles ? Bien sûr que oui !
$ cal 02 2020 | tr -d '[:space:]' | wc -c
75
$ cal 02 2021 | tr -d '[:space:]' | wc -c
73
Maintenant, affichons quelques années bissextiles !
$ 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
Des bidouilles sur des bidouilles, et ça marche !
Laquelle est la plus rapide, d'après toi ? Ouais, moi non plus je n'en ai aucune idée. L'approche « supprimer February » est peut-être plus rapide ? Mais je serais surpris qu'il y ait une grosse différence.
Ne restons pas dans le doute. Allons voir !
Il existe un excellent utilitaire appelé hyperfine qui permet d'évaluer les performances de plusieurs appels en ligne de commande
Laquelle est la plus rapide ? Hyperfine nous le dira
Voici les options que je passe à hyperfine, avec les deux commandes.
--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
On ne s'intéresse qu'à la mesure des performances du calcul d'une année, et non au temps nécessaire pour générer la boucle des années. Je vais donc mesurer uniquement la vérification du caractère bissextile pour une année donnée, avec chaque approche.
$ 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'
| Commande | Moyenne [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 |
Comme prévu, pratiquement aucune différence entre les deux en temps écoulé, MAIS l'approche « supprimer February » est peut-être légèrement plus rapide.
Et voilà qui met fin à ce voyage dans l'assemblage d'outils en ligne de commande.
Pour conclure
- On peut créer de nouvelles fonctionnalités en ligne de commande en assemblant des outils existants dans un pipeline de données
- Un peu de créativité permet d'obtenir des résultats étonnamment complexes à partir de pièces simples
- Hyperfine est un outil utile pour mesurer les performances des commandes en ligne de commande
Pour aller plus loin
- Explore l'utilisation de la commande
cal. - Essaie de construire un pipeline qui compte tous les caractères autres que les espaces pour une année entière :
cal 2020 - Essaie d'exécuter ce pipeline de comptage sur une année entière pour chacune des années de 1750 à 1759 : tu remarques quelque chose de bizarre ?