ושוב awk הציל את היום

04/12/2022

הבטחתי לעצמי שאני לא אעשה את כל ה Advent Of Code-ים השנה, ואני באמת מתכנן להפסיק - אבל עד שזה יקרה אני מקווה להשתמש בחידה של היום כדי להראות עוד כמה טריקים של יוניקס ו awk שאולי יעזרו לכם גם מחוץ לארץ האלפים.

1. מה צריך לחשב

אם יש לכם סבלנות לחפירות של אריק ווסטל אז אפשר לקרוא את התרגיל המקורי עם סיפורי אלפים באתר המשחק.

אבל בקבוצת הטלגרם ים תקצר את זה בצורה מופלאה אז אני פשוט מדביק כאן את הניסוח שלו:

לכל אות בטווח a-z יש ערך שנקרא "עדיפות", והוא 1–26 בהתאמה. כנ"ל A-Z, ערכים של 27–52 בהתאמה.

חלק 1: חלק כל שורה ל־2. בין החלק הראשון לחלק השני ישנה אות משותפת. מצא את סכום העדיפויות של האותיות המשותפות.

חלק 2: עבור כל 3 שורות צמודות בקובץ הקלט, בלי חפיפות (שורות 1–3, 4–6 וכן הלאה), ישנה אות משותפת בין השורות. מצא את סכום העדיפויות של כל האותיות הללו.

לדוגמה אם נתון הקלט:

vJrwpWtwJgWrhcsFMMfFFhFp
jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL
PmmdzqPrVvPwwTWBwg
wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn
ttgJtRGJQctTZtZT
CrZsJsPPZsGzwwsLwLmpwMDw

אז בחלק הראשון אנחנו צריכים לחלק כל שורה ל-2 בדיוק באמצע, ולמצוא איזה אות מופיעה בשני החצאים, ואז לסכום את האותיות שמצאנו בכל השורות לפי טבלת העדיפויות. בחלק השני צריך לחלק את הטקסט לקבוצות של שלוש שורות ולמצוא את האות המשותפת בין השורות.

2. עבודת הכנה

אנחנו הולכים להשתמש בכלים הסטנדרטיים של יוניקס בשביל לחשב את שני החלקים, אבל בשביל שהשורות לא יהיו יותר מדי ארוכות אני רוצה להכין שני קיצורים מראש.

קודם כל את טבלת העדיפויות של כל אות אפשר (כדאי) ליצור ולשמור לקובץ, כדי שיהיה קל יותר לחפש בה. זה הקוד שיוצר את הקובץ:

$ echo {a..z} {A..Z} | tr ' ' '\n'| cat -n > priority

כל שורה בקובץ מכילה מספר (העדיפות של האות) ואחריו האות שעליה מדברים:

$ head -4 priority
     1  a
     2  b
     3  c
     4  d

פינוק שני הוא ה alias שקורא שורות מ stdin ומחשב את סכום המספרים. השתמשתי בו גם בתרגיל של היום הראשון ובכל מקרה הוא alias שטוב שיהיה על המחשב:

$ alias sum='sed "/./s/^/+/" | tr -d "\n" | xargs echo 0 | bc '

מוכנים? אז יאללה אפשר להוציא את awk.

3. חלק 1

האתגר הראשון הוא לחלק כל שורה ל-2. ל awk יש פונקציה בשם substr שבשילוב עם length עושה את הקסם:

$ awk '{ print(substr($0, 0, length/2), substr($0, length/2+1)) }' demo.txt

vJrwpWtwJgWr hcsFMMfFFhFp
jqHRNqRjqzjGDLGL rsFMfFZSrLrFZsSL
PmmdzqPrV vPwwTWBwg
wMqvLMZHhHMvwLH jbvcjnnSBnvTQFn
ttgJtRGJ QctTZtZT
CrZsJsPPZsGz wwsLwLmpwMDw

פשוט הוספתי רווח בדיוק באמצע בין שני החצאים של השורה. אבל עכשיו אנחנו כבר בחצי הדרך לפיתרון. הצעד הבא הוא למצוא את האות שמופיעה בשני החצאים ובשביל זה אפשר להשתמש בביטוי רגולארי:

$ awk '{ print(substr($0, 0, length/2), substr($0, length/2+1)) }' demo.txt | egrep -o '([a-zA-Z]).* .*\1' | cut -c 1

p
L
P
v
t
s

בשביל להחליף כל אות בעדיפות שלה במהירות היה נחמד אם היתה לי טבלת Hash. אפשר להשתמש ב awk כדי לבנות אחת, אבל אני העדפתי לתת למחשב לעבוד קצת יותר ולהשאיר את הקוד יותר קצר אז פשוט שלחתי כל אות ל grep:

$ awk '{ print(substr($0, 0, length/2), substr($0, length/2+1)) }' demo.txt | egrep -o '([a-zA-Z]).* .*\1' | cut -c 1 | xargs -n 1 -I {} grep {} priority

    16  p
    38  L
    42  P
    22  v
    20  t
    19  s

ובסוף שוב awk כדי לקחת רק את העמודה הראשונה ולשלוח אותה ל alias הסכימה שיצרנו בהתחלה:

$ awk '{ print(substr($0, 0, length/2), substr($0, length/2+1)) }' demo.txt | egrep -o '([a-zA-Z]).* .*\1' | cut -c 1 | xargs -n 1 -I {} grep {} priority| awk '{ print $1}' | sum

157

4. חלק 2

בחלק השני אנחנו צריכים לקחת כל 3 שורות ולמצוא את האות המשותפת לשלושתן. ומי יותר טוב מ awk בשביל להוסיף שורה רווח אחרי כל שלוש שורות? המשתנה NR מכיל את מספר השורה, ולכן אני יכול לבדוק אם מספר השורה מתחלק ב-3:

$ awk '{ print } NR%3 == 0 { print "" }' demo.txt

vJrwpWtwJgWrhcsFMMfFFhFp
jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL
PmmdzqPrVvPwwTWBwg

wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn
ttgJtRGJQctTZtZT
CrZsJsPPZsGzwwsLwLmpwMDw

עכשיו שהנתונים מחולקים לקבוצות אפשר לאחד כל קבוצה לשורה ולהשתמש ב egrep כדי למצוא את האות המשותפת לשלושת המילים בשורה:

$ awk '{ print } NR%3 == 0 { print "" }' demo.txt| sed 's/^$/\x0/' | tr '\n' ' ' | xargs -0 -n 1 | egrep -o '([a-zA-Z]).* .*\1.* .*\1.*' | cut -c 1

r
Z

החלפה בעדיפות וסכימה בדיוק כמו בחלק הקודם נותנת לנו:

$ awk '{ print } NR%3 == 0 { print "" }' demo.txt| sed 's/^$/\x0/' | tr '\n' ' ' | xargs -0 -n 1 | egrep -o '([a-zA-Z]).* .*\1.* .*\1.*' | cut -c 1 | xargs -n 1 -I {} grep {} priority| awk '{ print $1 }' | sum

70

5. מה הלאה

אם הפוסט הזה עשה לכם חשק ללמוד awk יותר לעומק, תשמחו לשמוע שכבר כתבתי עליו מדריך יחסית מפורט בקישור כאן: https://www.tocode.co.il/blog/2022-06-awk.

ואם אתם רוצים לראות פיתרונות יותר רציניים לתרגילים האלה, או לשתף פיתרונות שלכם, מוזמנים להצטרף לקבוצה בטלגרם מהקישור בתחילת הפוסט.