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

۲۰۲۹ سال کبیسه نیست. این تطبیق فقط به خاطر ۲۹ موجود در ۲۰۲۹ رخ می‌دهد.

$ 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

آیا این با سال‌های کبیسه‌ی واقعی سازگار است؟

صفحه‌ی سال کبیسه‌ی ولفرام ریسرچ این فهرست را به عنوان سال‌های کبیسه‌ی نیمه‌ی اول قرن بیست‌ویکم آورده است.

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

تمام!

اما این جست‌وجوی ۲۹ با grep زیادی ساده است. بیایید عجیب‌ترش کنیم!

شناسایی سال‌های کبیسه، به روشی حتی هکی‌تر

تفاوت سال کبیسه با سال عادی، همان روز اضافه‌ی ۲۹ام است. آیا می‌توانیم تعداد نویسه‌های خروجی cal را بررسی کنیم تا سال‌های کبیسه را تشخیص دهیم؟

بله، می‌توانیم. اما اول باید از یک تله بگذریم. بگذارید نشانتان بدهم منظور چیست.

می‌دانیم ۲۰۲۰ سال کبیسه است و ۲۰۲۱ سال کبیسه نیست. بیایید تفاوت تعداد نویسه‌های خروجی فوریه‌ی هر یک از این سال‌ها را بررسی کنیم. این کار را با ابزار مفید wc انجام می‌دهیم، به‌طور خاص wc -c برای شمارش نویسه‌های بایتی.

$ printf "hello" | wc -c

5

۵ نویسه در hello (از printf به جای echo استفاده کردیم، چون در غیر این صورت یک نویسه‌ی خط جدید هم داشتیم). عالی!

$ cal 02 2020 | wc -c

184
$ cal 02 2021 | wc -c

184

همان سنگ‌لاخ اینجاست. هر ماه خروجی متفاوتی دارد (۲۰۲۰ آن روز ۲۹ام را دارد)، اما هر دو تعداد نویسه‌ی یکسانی دارند، چون cal فاصله اضافه می‌کند تا قالب‌بندی یکنواخت بماند. فاصله‌ها هم به اندازه‌ی عددها نویسه‌ی قابل شمارش‌اند.

چه می‌توانیم بکنیم؟

می‌توانیم همه‌ی فاصله‌ها را حذف کنیم! آن‌وقت فقط نویسه‌هایی می‌مانند که می‌خواهیم بشماریم.

ابزار عالی برای حذف نویسه‌ها، دستور tr است. دستور tr داده‌های رشته‌ای را تبدیل می‌کند. مثلاً می‌تواند همه‌ی نویسه‌های 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

ترفند روی ترفند، و کار می‌کند!

به نظر شما کدام سریع‌تر است؟ راستش من هم هیچ ایده‌ای ندارم. مسیر «حذف فوریه» شاید سریع‌تر باشد؟ اما تعجب می‌کنم اگر تفاوت چشمگیری وجود داشته باشد.

بگذارید تعجب نکنیم. بیایید بفهمیم!

ابزار عالی‌ای به نام 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'
دستور میانگین [ms] کمینه [ms] بیشینه [ms] نسبی
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

همان‌طور که انتظار می‌رفت، از نظر زمان واقعی تقریباً هیچ تفاوتی بینشان نیست، اما مسیر «حذف فوریه» ممکن است کمی سریع‌تر باشد.

و اینجا سفر سرهم‌کردن ابزارهای خط فرمان به پایان می‌رسد.

جمع‌بندی

  • می‌توانید با سرهم‌کردن ابزارهای موجود در قالب یک خط لوله‌ی داده، قابلیت‌های تازه‌ی خط فرمان بسازید
  • کمی خلاقیت می‌تواند از قطعه‌های ساده، نتایجی به‌طور غافلگیرکننده‌ای پیچیده بیرون بکشد
  • Hyperfine ابزار مفیدی برای سنجش عملکرد دستورهای خط فرمان است

قدم‌های بعدی

  • کار با دستور cal را کاوش کنید.
  • امتحان کنید خط لوله‌ای بسازید که همه‌ی نویسه‌های غیرفاصله‌ای یک سال کامل را می‌شمارد: cal 2020
  • همان خط لوله‌ی شمارش یک سال کامل را برای هر سال از ۱۷۵۰ تا ۱۷۵۹ اجرا کنید: نکته‌ی عجیبی متوجه می‌شوید؟
30 ژانویه 2024 · برایتان مفید بود؟