Uploaded avatar of sdball

Descobrir anos bissextos com o cal

@sdball
há Mais de 2 anos

Este artigo apareceu originalmente no site do Stephen e é republicado aqui (ligeiramente reescrito) com autorização

Introdução ao cal

Sabias que existe um calendário na linha de comandos do macOS e do 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

Uma consulta rápida ao tldr mostra como este utilitário pode ser útil

$ 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

Mas há uma coisa que ele não tem. Anos bissextos!

Claro que o cal sabe o que são anos bissextos, caso contrário não seria grande calendário. Mas não tem forma de os listar.

Reconhecer anos bissextos, de forma pouco ortodoxa

Vamos construí-lo nós próprios! Podíamos usar uma linguagem de programação a sério com uma biblioteca de calendários, mas construir coisas juntando componentes da linha de comandos é divertido, e não é como se fôssemos pôr este cálculo em produção.

Antes de mais, vamos concordar que fevereiro é o indicador de um ano bissexto.

$ 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

Podíamos procurar o "29" e fazer com que reconhecesse os anos bissextos? Talvez!

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

LEAP

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

normal

Até aqui tudo bem? Mas aposto que já vês o 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 NÃO é um ano bissexto. Só dá correspondência por causa do 29 em 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

Podíamos eliminar esse resultado problemático removendo da análise a linha inteira que contém "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

Agora, defendo que, se encontrarmos um "29" no resultado, então é MESMO um ano bissexto.

$ 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

Isto corresponde aos anos bissextos reais?

A página Ano Bissexto da Wolfram Research apresenta esta lista como os anos bissextos da primeira metade do século XXI.

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

Vamos ver como nos saímos

$ 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

Concluído!

Mas isto de procurar o 29 com grep é demasiado fácil. Vamos fazer algo mais estranho!

Reconhecer anos bissextos, de forma ainda menos ortodoxa

A diferença entre um ano bissexto e um ano normal é o dia extra, o 29. Podíamos verificar o número de carateres na saída do cal para reconhecer anos bissextos?

Podemos, sim. Mas primeiro há um obstáculo a ultrapassar. Deixa-me mostrar o que quero dizer.

Sabemos que 2020 é um ano bissexto e que 2021 não é. Vamos ver a diferença entre o número de carateres que cada um desses anos produz para fevereiro. Fazemo-lo com o prático utilitário wc, mais concretamente wc -c, para contar os carateres em bytes.

$ printf "hello" | wc -c

5

5 carateres em "hello" (usamos printf em vez de echo porque, caso contrário, teríamos também um caráter de nova linha). Ótimo!

$ cal 02 2020 | wc -c

184
$ cal 02 2021 | wc -c

184

Aí está o obstáculo. Cada mês tem uma saída diferente (2020 tem o tal dia 29), mas ambos têm o mesmo número de carateres, porque o cal acrescenta espaços para garantir uma formatação consistente. Os espaços são carateres contáveis tanto como os números.

O que podemos fazer?

Podemos remover todos os espaços! Assim ficamos apenas com os carateres que queremos contar.

Um ótimo utilitário para remover carateres é o comando tr. O comando tr traduz dados do tipo string. Por exemplo, pode transformar todos os carateres a em carateres A.

$ echo "aardvark" | tr a A

AArdvArk

Também pode apagar todos os carateres especificados, que é exatamente o que queremos.

No nosso caso, queremos apagar todos os carateres de espaço em branco, sejam quais forem (espaços, tabulações, novas linhas, etc.). Existe um agrupamento útil chamado classe de carateres que nos permite fazer exatamente isso. A classe de carateres [:space:] abrange todos esses diferentes tipos de espaços em branco.

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

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

February2021SuMoTuWeThFrSa12345678910111213141516171819202122232425262728

Ótimo! Já podemos contar o número de carateres para encontrar anos bissextos? Claro que podemos!

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

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

73

Agora vamos listar alguns anos bissextos!

$ 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

Hacks sobre hacks, e funciona!

Qual é que achas que é mais rápido? Pois, também não faço ideia. O caminho de "remover fevereiro" talvez seja mais rápido? Mas ficaria surpreendido se houvesse uma grande diferença.

Não vamos ficar surpreendidos. Vamos descobrir!

Existe um ótimo utilitário chamado hyperfine que permite avaliar o desempenho de várias chamadas de linha de comandos

Qual é mais rápido? O Hyperfine diz-nos

Estas são as opções que estou a dar ao hyperfine, juntamente com os dois 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

Só nos interessa medir o cálculo de um ano, e não o tempo necessário para gerar o ciclo de anos. Por isso, vou medir apenas a verificação do ano bissexto para um ano específico com cada abordagem.

$ 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 esperado, praticamente não há diferença entre eles em termos de tempo real, MAS o caminho de "remover fevereiro" pode ser ligeiramente mais rápido.

E assim termina esta viagem de montagem de ferramentas de linha de comandos.

Resumo

  • Podes criar novas funcionalidades de linha de comandos juntando ferramentas existentes numa cadeia de processamento de dados
  • Um pouco de criatividade consegue resultados surpreendentemente complexos a partir de peças simples
  • O Hyperfine é uma ferramenta útil para medir o desempenho de comandos de linha de comandos

Próximos passos

  • Explora a utilização do comando cal.
  • Tenta construir uma cadeia de processamento que conte todos os carateres que não sejam espaços de um ano inteiro: cal 2020
  • Tenta correr essa cadeia de contagem de um ano inteiro para cada um dos anos de 1750 a 1759: reparas em algo estranho?
30 de janeiro de 2024 · Foi útil?