Esta publicación apareció originalmente en el sitio web de Stephen y se republica aquí (ligeramente reescrita) con permiso
Introducción a cal
¿Sabías que hay un calendario en la línea de comandos de macOS y 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
Una comprobación rápida con tldr muestra lo útil que puede llegar a ser esta herramienta
$ 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
Pero hay una cosa que no tiene. ¡Los años bisiestos!
Cal sabe de años bisiestos, por supuesto; si no, no sería un buen calendario. Pero no tiene forma de listarlos.
Reconocer años bisiestos, de forma poco ortodoxa
¡Vamos a construirlo nosotros mismos! Claro que podríamos usar un lenguaje de programación de verdad con una biblioteca de calendarios, pero construir cosas ensamblando componentes de línea de comandos es divertido y tampoco vamos a poner este cálculo en producción.
Para empezar, pongámonos de acuerdo en que febrero es el indicador de un año bisiesto.
$ 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
¿Podríamos buscar «29» y conseguir que reconozca los años bisiestos? ¡Quizá!
$ cal 02 2020 | grep -q 29 && echo "LEAP" || echo "normal"
LEAP
$ cal 02 2021 | grep -q 29 && echo "LEAP" || echo "normal"
normal
¿Todo bien hasta ahora? Pero apuesto a que ves el problema.
$ 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 NO es un año bisiesto. Solo coincide por el 29 de 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
Podríamos eliminar ese resultado problemático quitando de la ecuación toda la línea que contiene «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
Ahora sostengo que si encontramos un «29» en el resultado, entonces es REALMENTE un año bisiesto.
$ 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
¿Coincide eso con los años bisiestos reales?
La página Año bisiesto de Wolfram Research tiene esta lista como los años bisiestos de la primera mitad del siglo XXI.
2000
2004
2008
2012
2016
2020
2024
2028
2032
2036
2040
2044
2048
Veamos cómo nos va
$ 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
¡Hecho!
Pero esto de buscar el 29 con grep es demasiado fácil. ¡Vamos a complicarlo más!
Reconocer años bisiestos, de forma aún más poco ortodoxa
La diferencia entre un año bisiesto y un año normal es ese día extra del 29. ¿Podríamos comprobar el número de caracteres de la salida de cal para reconocer los años bisiestos?
Sí, podemos. Pero antes hay que superar una pega. Déjame que te enseñe a qué me refiero.
Sabemos que 2020 es un año bisiesto y que 2021 no lo es. Comprobemos la diferencia entre el número de caracteres que genera febrero en cada uno de esos años. Lo haremos con la práctica herramienta wc, en concreto wc -c, para contar los bytes.
$ printf "hello" | wc -c
5
5 caracteres en «hello» (usamos printf en lugar de echo porque si no tendríamos también un carácter de nueva línea). ¡Genial!
$ cal 02 2020 | wc -c
184
$ cal 02 2021 | wc -c
184
Ahí está el obstáculo. Cada mes tiene una salida distinta (2020 tiene ese día 29), pero ambos tienen el mismo número de caracteres porque cal añade espacios para mantener un formato consistente. Los espacios son caracteres contables tanto como los números.
¿Qué podemos hacer?
¡Podemos quitar todos los espacios! Así solo quedarían los caracteres que queremos contar.
Una herramienta estupenda para eliminar caracteres es el comando tr. El comando tr traduce datos de tipo string. Por ejemplo, puede cambiar todos los caracteres a por caracteres A.
$ echo "aardvark" | tr a A
AArdvArk
También puede eliminar todos los caracteres que le especifiquemos, que es justo lo que queremos.
En nuestro caso queremos eliminar todos los caracteres de espacio en blanco, sean cuales sean (espacios, tabuladores, saltos de línea, etc.). Existe una agrupación muy útil llamada clase de caracteres que nos permite hacer exactamente eso. La clase de caracteres [:space:] abarca todos esos tipos distintos de espacios en blanco.
$ cal 02 2020 | tr -d '[:space:]'
February2020SuMoTuWeThFrSa1234567891011121314151617181920212223242526272829
$ cal 02 2021 | tr -d '[:space:]'
February2021SuMoTuWeThFrSa12345678910111213141516171819202122232425262728
¡Genial! ¿Podemos contar el número de caracteres para encontrar años bisiestos ahora? ¡Claro que podemos!
$ cal 02 2020 | tr -d '[:space:]' | wc -c
75
$ cal 02 2021 | tr -d '[:space:]' | wc -c
73
¡Ahora vamos a mostrar algunos años bisiestos!
$ 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
¡Trucos sobre trucos y funciona!
¿Cuál crees que es más rápido? Sí, yo tampoco tengo ni idea. Puede que la ruta de «quitar February» sea más rápida, pero me sorprendería que hubiera una gran diferencia.
No nos quedemos con la duda. ¡Vamos a averiguarlo!
Hay una herramienta estupenda llamada hyperfine que permite evaluar el rendimiento de varias llamadas a la línea de comandos
¿Cuál es más rápido? Hyperfine nos lo dirá
Estos son los flags que le paso a hyperfine junto con los dos comandos.
--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
Solo nos interesa medir el cálculo de un año, no el tiempo necesario para generar el bucle de años. Así que solo voy a medir la comprobación del año bisiesto para un año concreto con cada enfoque.
$ 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'
| Comando | Media [ms] | Mín [ms] | Máx [ms] | Relativa |
|---|---|---|---|---|
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 |
Como era de esperar, prácticamente no hay diferencia entre ellos en lo que respecta al tiempo real, PERO puede que la ruta de «quitar February» sea ligeramente más rápida.
Y así termina este viaje de ensamblaje de herramientas de línea de comandos.
En resumen
- Puedes crear nuevas funcionalidades de línea de comandos ensamblando herramientas existentes en una tubería de datos
- Un poco de creatividad puede conseguir resultados sorprendentemente complejos a partir de piezas simples
- Hyperfine es una herramienta útil para medir el rendimiento de comandos de línea de comandos
Próximos pasos
- Explora el uso del comando
cal. - Intenta construir una tubería que cuente todos los caracteres que no sean espacios de un año entero:
cal 2020 - Intenta ejecutar esa tubería de conteo de un año entero para cada uno de los años de 1750 a 1759: ¿notas algo raro?