Este post foi publicado originalmente no site do Stephen e é republicado aqui (levemente reescrito) com permissão
Introdução ao cal
Você sabia que existe um calendário na linha de comando 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 olhada rápida no tldr mostra como esse 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 tem uma coisa que ele não tem. Anos bissextos!
O cal conhece anos bissextos, é claro, senão não seria bem um calendário. Mas ele não tem como listá-los.
Reconhecendo anos bissextos, na gambiarra
Vamos construir isso nós mesmos! Claro que poderíamos usar uma linguagem de programação de verdade com uma biblioteca de calendário, mas montar coisas juntando componentes de linha de comando é divertido, e não é como se fôssemos colocar esse cálculo em produção.
Antes de tudo, vamos combinar que fevereiro é o indicador de 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
Daria para procurar por "29" e deixar que ele reconheça 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é aí tudo bem? Mas aposto que você já viu 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. Ele só corresponde 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
Poderíamos eliminar esse resultado problemático tirando de consideração a linha inteira com "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 eu sustento que, se encontrarmos um "29" no resultado, então é REALMENTE 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
Isso bate com os anos bissextos de verdade?
A página Leap Year da Wolfram Research traz 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
Pronto!
Mas esse negócio de grep no 29 é fácil demais. Vamos ficar mais esquisitos!
Reconhecendo anos bissextos, ainda mais na gambiarra
A diferença entre um ano bissexto e um ano normal é aquele dia extra, o 29. Daria para checar o número de caracteres da saída do cal para reconhecer anos bissextos?
Dá sim. Mas tem uma pegadinha para passar primeiro. Deixa eu mostrar o que quero dizer.
Sabemos que 2020 é um ano bissexto e que 2021 não é. Vamos comparar o número de caracteres gerados para fevereiro de cada um desses anos. Fazemos isso com o prático utilitário wc, mais especificamente wc -c, para contar os caracteres em bytes.
$ printf "hello" | wc -c
5
5 caracteres em "hello" (usamos printf em vez de echo porque senão teríamos também um caractere 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 aquele dia 29), mas os dois têm o mesmo número de caracteres porque o cal adiciona espaços para garantir uma formatação consistente. Espaços são caracteres contáveis tanto quanto números.
O que podemos fazer?
Podemos remover todos os espaços! Assim sobram só os caracteres que queremos contar.
Um ótimo utilitário para remover caracteres é o comando tr. O comando tr traduz dados do tipo string. Por exemplo, ele pode transformar todos os caracteres a em caracteres A.
$ echo "aardvark" | tr a A
AArdvArk
Ele também pode apagar todos os caracteres especificados, que é exatamente o que queremos.
No nosso caso, queremos apagar todos os caracteres de espaço em branco, sejam quais forem (espaços, tabulações, novas linhas etc.). Existe um agrupamento útil chamado classe de caracteres que permite fazer exatamente isso. A classe de caracteres [:space:] atinge todos esses tipos diferentes de espaço em branco.
$ cal 02 2020 | tr -d '[:space:]'
February2020SuMoTuWeThFrSa1234567891011121314151617181920212223242526272829
$ cal 02 2021 | tr -d '[:space:]'
February2021SuMoTuWeThFrSa12345678910111213141516171819202122232425262728
Ótimo! Dá para contar o número de caracteres para encontrar anos bissextos agora? Com certeza!
$ cal 02 2020 | tr -d '[:space:]' | wc -c
75
$ cal 02 2021 | tr -d '[:space:]' | wc -c
73
Agora vamos imprimir 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
Gambiarra sobre gambiarra, e funciona!
Qual você acha que é mais rápido? Pois é, eu também não faço ideia. O caminho que "remove o February" talvez seja mais rápido? Mas eu ficaria surpreso se houvesse uma grande diferença.
Nada de suposições. Vamos descobrir!
Existe um ótimo utilitário chamado hyperfine que permite avaliar o desempenho de várias chamadas de linha de comando
Qual é mais rápido? O hyperfine nos conta
Aqui estão as flags que estou passando para o hyperfine, junto 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
Estamos interessados apenas em medir o cálculo de um ano, e não o tempo necessário para gerar o laço de anos. Então vou medir apenas a checagem 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 nenhuma diferença entre eles em termos de tempo de relógio, MAS o caminho que "remove o February" pode ser ligeiramente mais rápido.
E assim chega ao fim esta jornada de montar ferramentas de linha de comando.
Resumindo
- Você pode criar novos recursos de linha de comando montando ferramentas existentes em um pipeline 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 comando
Próximos passos
- Explore o uso do comando
cal. - Tente montar um pipeline que conta todos os caracteres que não são espaços de um ano inteiro:
cal 2020 - Tente rodar esse pipeline de contagem do ano inteiro para cada um dos anos de 1750 a 1759: percebe algo estranho?