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?