Uploaded avatar of sdball

Encontrar años bisiestos con cal

@sdball
hace Más de 2 años

Este artículo apareció originalmente en el sitio web de Stephen y se republica aquí (con ligeros cambios) 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 revisión rápida de tldr muestra lo útil que puede 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 algo que no tiene. ¡Los años bisiestos!

Claro que cal sabe de los años bisiestos; si no, no serviría de mucho como calendario. Pero no tiene forma de listarlos.

Reconocer años bisiestos, de forma poco ortodoxa

¡Construyámoslo nosotros mismos! Claro que podríamos usar un lenguaje de programación de verdad con una biblioteca de calendario, pero construir cosas ensamblando componentes de línea de comandos es divertido, y tampoco es que vayamos a poner este cálculo en producción.

Para empezar, aceptemos 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 hacer 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 ya 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. Coincide solo por el 29 que aparece en 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 si quitamos de la jugada toda la línea con «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 SÍ es un año bisiesto de verdad.

$ 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 Leap Year 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

¡Listo!

Pero esto de buscar el 29 con grep es demasiado fácil. ¡Hagámoslo aún más raro!

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 revisar la cantidad de caracteres en la salida de cal para reconocer los años bisiestos?

Sí, podemos. Pero primero hay una trampa que superar. Déjame mostrarte a qué me refiero.

Sabemos que 2020 es un año bisiesto y que 2021 no lo es. Veamos la diferencia entre la cantidad de caracteres que genera febrero en cada uno de esos años. Lo haremos con la práctica utilidad wc, en concreto con wc -c para contar los caracteres de bytes.

$ printf "hello" | wc -c

5

5 caracteres en «hello» (usamos printf en vez de echo porque de lo contrario también tendríamos un carácter de nueva línea). ¡Genial!

$ cal 02 2020 | wc -c

184
$ cal 02 2021 | wc -c

184

Ahí está el tropiezo. Cada mes tiene una salida distinta (2020 tiene ese día 29), pero ambos tienen la misma cantidad de caracteres porque cal agrega espacios para garantizar un formato uniforme. Los espacios son caracteres contables tanto como los números.

¿Qué podemos hacer?

¡Podemos quitar todos los espacios! Así quedarían solo los caracteres que queremos contar.

Una gran utilidad para quitar 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 se especifiquen, que es justo lo que queremos.

En nuestro caso queremos eliminar todos los caracteres de espacio en blanco, sin importar cuáles sean (espacios, tabulaciones, saltos de línea, etc.). Hay una agrupación muy útil llamada clase de caracteres que nos permite hacer justo eso. La clase de caracteres [:space:] apuntará a todos esos distintos tipos de espacios en blanco.

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

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

February2021SuMoTuWeThFrSa12345678910111213141516171819202122232425262728

¡Genial! ¿Podemos contar la cantidad de caracteres para encontrar años bisiestos ahora? ¡Claro que sí!

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

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

73

¡Ahora mostremos 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 idea. ¿Quizá el camino de «quitar February» es más rápido? Pero me sorprendería que hubiera una gran diferencia.

No nos quedemos con la duda. ¡Averigüémoslo!

Hay una gran utilidad llamada hyperfine que permite evaluar el rendimiento de varias llamadas de línea de comandos.

¿Cuál es más rápido? Hyperfine nos lo dirá

Estas son las opciones que le doy 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 que se tarda en generar el bucle de años. Así que solo voy a medir la comprobación del año bisiesto para un año específico 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'
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

Como era de esperar, prácticamente no hay diferencia entre ellos en cuanto al tiempo real, PERO el camino de «quitar February» puede ser un poco más rápido.

Y así llega a su fin este viaje de ensamblar herramientas de línea de comandos.

Para terminar

  • Puedes crear nuevas funcionalidades de línea de comandos ensamblando herramientas existentes en una canalización de datos
  • Un poco de creatividad puede lograr resultados sorprendentemente complejos a partir de piezas sencillas
  • 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 canalización que cuente todos los caracteres que no son espacios de un año completo: cal 2020
  • Intenta ejecutar esa canalización de conteo de año completo para cada uno de los años desde 1750 hasta 1759: ¿notas algo raro?
Translation missing: es-419.number.nth.ordinalized Jan 2024 · ¿Te resultó útil?