Uploaded avatar of sdball

Βρίσκοντας δίσεκτα έτη με το cal

@sdball
πριν από Πάνω από 2 χρόνια

Αυτό το άρθρο εμφανίστηκε αρχικά στην ιστοσελίδα του Stephen και αναδημοσιεύεται εδώ (ελαφρώς ξαναγραμμένο) με άδεια

Εισαγωγή στο 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 ξέρει φυσικά για τα δίσεκτα έτη, αλλιώς δε θα ήταν και πολύ ημερολόγιο. Αλλά δεν έχει τρόπο να τα απαριθμήσει.

Αναγνωρίζοντας τα δίσεκτα έτη, με κόλπα

Ας το φτιάξουμε μόνοι μας! Σίγουρα θα μπορούσαμε να χρησιμοποιήσουμε μια πραγματική γλώσσα προγραμματισμού με μια βιβλιοθήκη ημερολογίου, αλλά το να φτιάχνεις πράγματα συνθέτοντας components της γραμμής εντολών έχει πλάκα, και δεν πρόκειται να βάλουμε αυτόν τον υπολογισμό σε παραγωγή.

Πρώτα απ' όλα, ας συμφωνήσουμε ότι ο Φεβρουάριος είναι η ένδειξη για ένα δίσεκτο έτος.

$ 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

Ταιριάζει αυτό με τα πραγματικά δίσεκτα έτη;

Η σελίδα Leap Year της Wolfram Research έχει αυτή τη λίστα ως τα δίσεκτα έτη στο πρώτο μισό του 21ου αιώνα.

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 για το 29 είναι πολύ εύκολο. Ας το κάνουμε πιο περίεργο!

Αναγνωρίζοντας τα δίσεκτα έτη, με ακόμα περισσότερα κόλπα

Η διαφορά ανάμεσα σε ένα δίσεκτο και ένα κανονικό έτος είναι αυτή η επιπλέον ημέρα, η 29η. Θα μπορούσαμε να ελέγξουμε τον αριθμό των χαρακτήρων στην έξοδο του cal για να αναγνωρίζουμε τα δίσεκτα έτη;

Ναι, μπορούμε. Αλλά υπάρχει μια παγίδα που πρέπει πρώτα να ξεπεράσουμε. Άσε με να σου δείξω τι εννοώ.

Ξέρουμε ότι το 2020 είναι δίσεκτο έτος και το 2021 δεν είναι δίσεκτο έτος. Ας ελέγξουμε τη διαφορά ανάμεσα στον αριθμό των χαρακτήρων που παράγονται για τον Φεβρουάριο καθενός από αυτά τα έτη. Θα το κάνουμε με το εύχρηστο εργαλείο wc, συγκεκριμένα με το wc -c για να μετρήσουμε τους χαρακτήρες byte.

$ 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 μετατρέπει δεδομένα συμβολοσειρών. Για παράδειγμα, μπορεί να αλλάξει όλους τους χαρακτήρες 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 θα μας πει

Ορίστε τα flags που δίνω στο 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

Όπως αναμενόταν, πρακτικά καμία διαφορά μεταξύ τους όσον αφορά τον πραγματικό χρόνο εκτέλεσης, ΑΛΛΑ η διαδρομή "αφαίρεση του February" μπορεί να είναι ελαφρώς πιο γρήγορη.

Και έτσι φτάνει στο τέλος της αυτό το ταξίδι συναρμολόγησης εργαλείων γραμμής εντολών.

Κλείνοντας

  • Μπορείς να φτιάξεις νέες λειτουργίες γραμμής εντολών συνθέτοντας υπάρχοντα εργαλεία σε ένα pipeline δεδομένων
  • Λίγη δημιουργικότητα μπορεί να δώσει εντυπωσιακά σύνθετα αποτελέσματα από απλά κομμάτια
  • Το Hyperfine είναι ένα χρήσιμο εργαλείο για τη σύγκριση εντολών γραμμής εντολών

Επόμενα βήματα

  • Εξερεύνησε τη χρήση της εντολής cal.
  • Δοκίμασε να φτιάξεις ένα pipeline που μετράει όλους τους χαρακτήρες που δεν είναι κενά για ένα ολόκληρο έτος: cal 2020
  • Δοκίμασε να τρέξεις αυτό το pipeline μέτρησης ολόκληρου του έτους για κάθε έτος από το 1750 έως το 1759: παρατηρείς κάτι περίεργο;
Translation missing: el.number.nth.ordinalized Jan 2024 · Σου φάνηκε χρήσιμο;