לאימות תאריך לידה יש מוניטין של משימה קלה, ומוניטין זה אינו מוצדק. השדה מקבל ערך אחד, הערך הוא תאריך, ותאריך הוא בעיה פתורה—אלא שהיא אינה פתורה, מפני שתאריך לידה הוא גם הקלט לחישוב גיל, הגיל מושווה לסף, וגם הגיל וגם הסף תלויים בשאלה מהו היום במקום שבו ההשוואה מתבצעת.
המאמר הזה מלקט את מקרי הקצה שבהם מימוש שנראה נכון מפיק תשובה שגויה, ומסביר אילו מהם שייכים לחבילת בדיקות לפני שחרור ולא אחרי תלונה.
מה מייחד תאריך לידה משאר התאריכים?
תאריך הזמנה, תאריך חשבונית ותאריך אספקה יושבים כולם בקרבת ההווה, ואף אחד מהם אינו צריך להיות מומר למספר של שנים. תאריך לידה שונה בשלושה מובנים בבת אחת:
- הוא רחוק בעבר, ולכן הוא משתרע על עשרות מחזורי שנים מעוברות ועל לפחות שינוי כלל אחד
- הוא משמש לחישוב גילו של אדם, כלומר אריתמטיקה ולא עיצוב
- הוא מושווה לעיתים קרובות לסף חוקי, כלומר לתשובה יש השלכות
התוצאה של טעות אינה סימטרית. דחיית תאריך לידה תקף מרחיקה משתמש חוקי; קבלת תאריך לידה שהיה צריך להיכשל מאפשרת למישהו לעבור שער שקיים כדי להגן עליו. שתי אלה כשלים ממשיים, והם מגיעים במסלולי קוד שונים.
אילו תאריכים אינם קיימים כלל?
כל תאריך שאינו קיים בלוח השנה צריך להידחות, וזה נשמע מובן מאליו עד שכותבים את כלל השנה המעוברת במלואו. שנה היא שנה מעוברת כאשר היא מתחלקת בארבע, אלא ששנות מאה חייבות להתחלק בארבע מאות, ולכן שנת מאה אחת שנראית מתחלקת בארבע אינה שנה מעוברת ואחרת כן.
מקרי הקצה המעשיים שחשובים הם שתי שנות המאה משני צדי ההווה. אחת מהן אינה שנה מעוברת, ולכן היום העשרים ותשעה בחודש השני שלה אינו תאריך תקף; האחרת היא שנה מעוברת, ולכן אותו יום תקף. מימושים שמיישמים רק את הכלל של התחלקות בארבע מקבלים תאריך בלתי אפשרי אחד ודוחים תאריך אמיתי אחד, והשגיאה בלתי נראית ברוב ימות השנה.
אותו היגיון חל על שאר לוח השנה: לחודשים אורכים שונים, חלק מהקלטים מגיעים בסדר של יום תחילה וחלק בסדר של חודש תחילה, ושנה בת שתי ספרות היא דו משמעית מעצם הגדרתה. שדה שמקבל שנה בת שתי ספרות בלי הקשר יציב אנשים במרחק של מאה שנה מהגיל האמיתי שלהם.
כיצד מחשבים גיל בפועל?
גיל הוא הפרש של שנים שלמות, שמחושב על ידי השוואת תאריך הלידה לתאריך הנוכחי רכיב אחר רכיב: מחסירים את שנות הלידה, ואז מחסירים אחת נוספת אם יום ההולדת עוד לא הגיע השנה. יום ההולדת עצמו הוא הגבול, והמוסכמה הנפוצה היא שאדם גדל בשנה ביום עצמו ולא ביום שאחריו.
להגדרה הזאת יש דקות שכדאי לציין, מפני ששם יושבות רוב שגיאות הסטייה באחת. ההשוואה היא בין שני תאריכי לוח שנה, ולא בין שני רגעים. שני אנשים שנולדו באותו יום בלוח השנה הם בני אותו גיל באותו יום גם אם נולדו בשעות שונות, ומי שנולד בשעות הערב המאוחרות אינו גדל בשנה בשעה הזאת.
המרה של אחד התאריכים למניין שניות וחלוקה היא המימוש השגוי הרגיל. הוא נסחף על פני שנים מעוברות, הוא הופך את התשובה לתלויה בשעה ביום, והוא מפיק גיל שמשתנה בשעה שרירותית ולא בחצות.
איזה תאריך נוכחי משמשת הבדיקה?
זה של השעון שהשרת נמצא עליו, אלא אם מישהו החליט אחרת. זהו השורש של סוג שלם של תקלות שמופיעות רק סביב חצות ורק אצל משתמשים באזורי זמן מסוימים: תאריך לידה שמספק את סף הגיל עבור היום המקומי של המשתמש נכשל עבור היום של השרת, או להפך.
הדרך הנקייה לחשוב על זה היא שתאריך הלידה הוא תאריך ואינו נושא שעה כלל. יש לשמור אותו כתאריך רגיל בלי אזור זמן, ולבצע את השוואת הגיל מול תאריך שנגזר ממדיניות מפורשת—בדרך כלל התאריך המקומי של המשתמש לבדיקה אינטראקטיבית, ותאריך ייחוס קבוע לכל דבר שצריך להיות ניתן לשחזור. ערבוב של תאריך לידה בלי אזור זמן עם רגע נוכחי עם אזור זמן הוא הדרך שבה הדו משמעות נכנסת.
יש מלכודת קשורה לבדיקות. בדיקה שמחשבת את התוצאה המצופה שלה משעון המערכת עוברת היום ונכשלת ביום ההולדת של מישהו, או בשנה מעוברת, או אחרי שינוי מדיניות. בדיקות שטוענות טענה על גיל זקוקות לתאריך נוכחי מוזרק ולא נקרא.
האם כל לוחות השנה מסכימים על אותו יום?
לא. ספירת הזמן שבה משתמש לוח השנה הגלובלי הנפוץ אינה היחידה בשימוש, וכמה אזורים מקיימים מערכות משלהם לצורכי אזרחות. תאריך הכתוב בלוח שנה אחד אינו מתאים לאותו תאריך כתוב בלוח שנה אחר, ואותו רגע יכול להופיע עם שנה, חודש ויום שונים בהתאם למערכת שהטופס מצפה לה.
למי שבונה טפסים, הכלל המעשי הוא להיות מפורש ולא חכם: יש להצהיר איזה לוח שנה השדה מצפה לו, לקבל את הרכיבים בסדר מוצהר, ואם לוח השנה המקומי של אזור חשוב למשתמשים, יש להתייחס להמרה כהחלטת מוצר עם שדה משלה ולא כהמרה אוטומטית שאיש אינו רואה. המרה שקטה גרועה מאי המרה, מפני שערך שגוי אינו ניתן להבחנה מערך שהוקלד.
היכן תאריכי מציין מקום יוצרים צרות?
שלושה ערכי ברירת מחדל גורמים לנזק מדיד. היום הראשון בינואר של שנה עגולה הוא מציין המקום הנפוץ בעולם, וטופס שמתייחס אליו בשקט כתאריך לידה אמיתי יזכה לאשכול משתמשים שכולם מתגלים כבעלי אותו יום הולדת. ערך אפס או ריק שמתחזה לתאריך גרוע יותר, מפני שהוא יכול להפיק גיל של כמה מאות שנים ולעבור בדיקת גיל מעל שלוש עשרה בזמן שהוא נכשל בשקט בכל בדיקה אחרת.
השלישי הוא מציין המקום שתקף מבחינה טכנית ושגוי בעליל, כמו התאריך המוקדם ביותר שבורר התאריכים מאפשר. לשלושתם תסמין משותף: הם גורמים לרשומה גרועה להיראות שלמה, ולכן קוד במורד הזרם אינו זוכה להזדמנות לדחות אותה.
זו הטענה בעד רשומות מופקות בבדיקות. ערכים שנבנים על ידי מחולל הזהות ונתוני הבדיקה פרוסים ולא מקובצים, כלומר טופס שנמצא בבדיקה רואה טווח של תאריכים בעלי צורה אמיתית במקום אותו מציין מקום שוב ושוב. אלה ערכים בדיוניים לבדיקות תוכנה ולא יותר מכך: לא תאריך הלידה האמיתי של איש, ולא מסמך שיכול לשמש כזהות של מישהו.
למפתחים: גבולות שכדאי לטעון עליהם
יש לקבע את תאריך הייחוס בבדיקה, ואז לטעון טענה על הגבול המדויק ולא על משהו בקרבתו. ארבעה ערכים מכסים את רוב הסיכון בכל סף: תאריך הלידה שהופך את האדם לגיל הסף בדיוק היום, זה שקצר ביום אחד, זה שחרג ביום אחד, וזה שנולד ביום מעובר.
מעבר לאלה, יש להשאיר את כללי האחסון וההשוואה צרים: לשמור תאריך לוח שנה בלי אזור זמן, לגזור את הגיל לפי דרישה במקום לשמור אותו, ולעולם לא לאפשר לתאריך מציין מקום להפוך לבלתי ניתן להבחנה מתאריך אמיתי. ערך סימן ייחוס מחוץ לטווח הסביר, שנדחה על ידי האימות, עדיף על ברירת מחדל סבירה שחומקת מבדיקות. וכדאי להשאיר מספרי מדיניות בקונפיגורציה, מפני שספים משתנים וסף מקודד קשיח הוא שחרור אחד מהפיכה לשגוי.
הצעדים הבאים
יש לקחת את בדיקת הגיל במוצר ולהריץ דרכה את ארבעת ערכי הגבול עם תאריך ייחוס קבוע; אם אחד מהם מחזיר תשובה שגויה, התקלה נמצאת בהשוואה ולא בטופס. לאחר מכן יש לשלוף מגוון תאריכי לידה מופקים ממחולל הזהות ולאשר שהשדה שומר אותם ללא שינוי, כולל תאריך בתחילת חודש ותאריך בסדר לוח שנה שאינו ברירת המחדל, כדי ששינוי שפה לא יסדר אותם מחדש בשקט. המאמר על בדיקות אימות גיל מכסה את הספים שתאריכים אלה מושווים אליהם בדרך כלל.