Uploaded avatar of sdball

Пошук високосних років за допомогою cal

@sdball
Більше 2 років тому

Цей допис уперше зʼявився на сайті Стівена і публікується тут (трохи переписаний) з дозволу

Знайомство з cal

Чи відомо, що в командному рядку macOS і 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

Швидкий погляд на tldr показує, наскільки корисною може бути ця утиліта

$ 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

Але є одна річ, якої в неї немає. Високосні роки!

Cal, звісно, знає про високосні роки, інакше це був би не календар. Але в нього немає способу перелічити їх.

Розпізнавання високосних років хакерським способом

Збудуймо це самі! Авжеж, ми могли б скористатися справжньою мовою програмування з бібліотекою календаря, але створювати речі, складаючи компоненти командного рядка, цікаво, і ми ж не збираємося запускати це обчислення у виробництво.

Спершу погодьмося, що лютий є показником високосного року.

$ 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

Чи могли б ми пошукати «29» і змусити це розпізнавати високосні роки? Можливо!

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

LEAP

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

normal

Поки що все добре? Але я впевнений, що проблема вже помітна.

$ 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 рік НЕ високосний. Він збігається лише через 29 у 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

Ми могли б прибрати цей проблемний результат, вилучивши з розгляду весь рядок із «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

Тепер я вважаю, що якщо ми знаходимо «29» у результаті, то це СПРАВДІ високосний рік.

$ 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

Чи збігається це з реальними високосними роками?

Сторінка «Високосний рік» від Wolfram Research наводить цей список як високосні роки першої половини XXI століття.

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

Погляньмо, як ми впораємося

$ 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

Готово!

Але цей пошук 29 занадто простий. Ускладнімо ще більше!

Розпізнавання високосних років ще хакерськішим способом

Різниця між високосним і звичайним роком полягає в тому додатковому дні 29-го. Чи могли б ми перевіряти кількість символів у виводі cal, щоб розпізнавати високосні роки?

Так, могли б. Але спершу треба подолати одну перешкоду. Покажу, що я маю на увазі.

Ми знаємо, що 2020 рік високосний, а 2021 не високосний. Перевірмо різницю між кількістю символів, виведених для лютого кожного з цих років. Зробимо це за допомогою зручної утиліти wc, а саме wc -c, щоб порахувати байти.

$ printf "hello" | wc -c

5

5 символів у «hello» (використовуємо printf замість echo, бо інакше мали б ще й символ нового рядка). Чудово!

$ cal 02 2020 | wc -c

184
$ cal 02 2021 | wc -c

184

Ось і камінь спотикання. Кожен місяць має різний вивід (у 2020 є той 29-й день), але в них однакова кількість символів, бо cal додає пробіли для однакового форматування. Пробіли такі ж лічені символи, як і цифри.

Що ми можемо зробити?

Ми можемо прибрати всі пробіли! Тоді залишаться лише символи, які ми хочемо порахувати.

Для вилучення символів чудово підходить команда tr. Команда tr перетворює рядки тексту (англ. string). Наприклад, вона може змінити всі символи a на символи A.

$ echo "aardvark" | tr a A

AArdvArk

Вона також може видаляти всі вказані символи, а це саме те, що нам потрібно.

У нашому випадку ми хочемо видалити всі пробільні символи, якими б вони не були (пробіли, табуляції, нові рядки тощо). Є корисне групування під назвою клас символів, яке дає змогу зробити саме це. Клас символів [:space:] націлений на всі ці різновиди пробільних символів.

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

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

February2021SuMoTuWeThFrSa12345678910111213141516171819202122232425262728

Чудово! Чи можемо ми тепер порахувати кількість символів, щоб знайти високосні роки? Авжеж можемо!

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

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

73

Тепер виведімо кілька високосних років!

$ 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

Хак на хакі, і воно працює!

Що, на нашу думку, швидше? Так, я й сам не маю гадки. Шлях із вилученням «February» може бути швидшим? Але я здивуюся, якщо різниця буде великою.

Не будьмо здивовані. Зʼясуймо!

Є чудова утиліта hyperfine, яка дає змогу оцінити продуктивність кількох викликів командного рядка

Що швидше? Hyperfine підкаже

Ось прапорці, які я передаю hyperfine разом із двома командами.

--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

Нас цікавить лише вимірювання швидкодії обчислення року, а не час, потрібний для генерації циклу років. Тому я вимірюю лише перевірку високосного року для конкретного року кожним підходом.

$ 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'
Команда Середнє [мс] Мін. [мс] Макс. [мс] Відносна
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

Як і очікувалося, практично жодної різниці між ними за реальним часом, АЛЕ шлях із вилученням «February» може бути трохи швидшим.

На цьому подорож зі складання інструментів командного рядка завершується.

Підсумки

  • Ми можемо створювати нові можливості командного рядка, складаючи наявні інструменти в конвеєр даних
  • Трохи творчості дає змогу отримати напрочуд складні результати з простих частин
  • За допомогою Hyperfine можна вимірювати швидкодію команд командного рядка

Наступні кроки

  • Дослідіть використання команди cal.
  • Спробуйте побудувати конвеєр, який рахує всі символи, що не є пробілами, за цілий рік: cal 2020
  • Спробуйте запустити цей конвеєр підрахунку за цілий рік для кожного року з 1750 по 1759: чи помітили щось дивне?
Translation missing: uk.number.nth.ordinalized Jan 2024 · Виявилося корисним?