เรื่องประหลาดๆ ของการนับปีใน Java

เรื่องประหลาดๆ ของการนับปีใน Java

ลืมหัวข้อ blog นี้ไปก่อนนะครับ แล้วลองพยายามเดาดูว่าโค้ด Java ข้างล่างนี้พยายามทำอะไร มันก็ดูตรงไปตรงมาใช่มั้ยครับ แล้วถ้ารันออกมาสมมติวันนี้วันที่ 1 ธันวาคม 2021 สิ่งที่ function นี้คืนกลับมาก็

SomeUtils_java.png

เป็น 2020-12-01 ใช่มั้ยครับ ถ้าเราเขียน Unit test ไว้ เทสนี้ก็น่าจะผ่านได้ง่ายๆ ปกติไม่มีอะไร

เรื่องมันมีอยู่ว่า ถ้าสมมติวันนี้เป็นวันที่ 31 ธันวาคม 2021 แล้วโค้ดชุดนี้เอาไว้บอกว่าควรจะจ่ายเงินเดือนเราตอนวันที่เท่าไร ลองทายดูมั้ยครับว่าจะเกิดอะไรขึ้น

รู้จักกับ DateTimeFormatter กันก่อน

ผมสังเกตุเห็น อย่างน้อยก็ใน Codebase ที่ผมทำงานด้วยอยู่ว่า Java Developer ส่วนใหญ่จะยังคุ้นเคยกับ Date API ที่มีมาตั้งแต่ Java 1.0 หน้าตาประมาณโค้ดข้างล่างครับ

SomeOldUtils_java.png

ซึ่ง จะเห็นได้ว่า Date API ตัวเก่าของ Java นั้นมีปัญหาเยอะมาก อย่างเช่นการที่จะปั้นเวลาซักอย่างก็เริ่มที่ปี 1900 แล้วบวกไปเอาหรือเดือนที่เริ่มที่ index ที่ 0 ซึ่งไม่ใช่ว่าทางทีมพัฒนา Java จะไม่พยายามแก้ปัญหานี้นะครับ ความพยายามแรกคือ Calendar class ที่ออกมาใน Java 1.1 แต่ก็ยังมีปัญหาหลายๆ อย่างอยู่ จน Java Developer หลายคนเลือกที่จะไปใช้ 3rd Party Library อย่าง Joda-Time แทน ซึ่งหลังจาก Oracle ได้ยึด Java ไปก็เลยเกิด Date/Time API ตัวใหม่ขึ้นมาใน Java 8 ซึ่งมีพื้นฐานหลายๆ อย่างมาจาก Joda-Time ครับ

ซึ่งใน API ตัวใหม่นอกจาก LocalDate, LocalTime ฯลฯ ตัว API ยังมาพร้อมกับ Formatter กับ Parser ตัวใหม่ด้วยครับชื่อว่า DateTimeFormatter ซึ่งช่วยให้เรา Parse เวลาเข้ามาได้ง่ายขึ้นพร้อมกับ constants อีกจำนวนนึงที่ช่วยให้เราประหยัดเวลาในการปั้น Date format ไปได้เยอะเลย แถมยัง Thread-safe ด้วยนะ

Pattern letters and Symbols for Formatting and Parsing

เวลาที่เรา Format Date/Time เนี่ย pattern ส่วนใหญ่ที่เราต้องทำคงหนีไม่พ้น yyyy-MM-dd ดูเผินๆ มันก็ตรงตัวใช่มั้ยครับ อาจจะมีแค่ M เดือน (Month-of-year) ที่ต้องเปลี่ยนไปใช้ M ใหญ่ เพราะมันดันไปพ้องกับอีก m นึงในหน่วยเวลาคือ นาที (minute-of-hour)

Screen Shot 2564-12-01 at 22.07.06.png

ซึ่งพวกเราน่าจะคุ้นเคยกับ pattern พวกนี้ดีอยู่แล้วใช่มั้ยครับ แล้วผมจะเขียนบล็อกนี้มาทำไมนะ ผมถามอีกทีว่า พวกเราน่าจะคุ้นเคยกับ pattern พวกนี้ดีอยู่แล้วใช่มั้ยครับ?

พอดีวันนี้ ในขณะที่ผมไต่ไปตาม codebase ที่ผมดูแลอยู่ผมไปสะดุดกับ pattern หน้าตาแบบภาพบนสุดเลยครับคือ YYYY-MM-dd ซึ่ง ถ้ามองผ่านๆ ผมก็อาจจะไม่คิดอะไร แต่ผมดันไปเอะใจว่า ผมเคยเห็นแต่ yyyy-MM-dd นี่นา แล้ว YYYY กับ yyyy มันต่างกันยังไงนะ

เรื่องของปี (ต่างๆ)

ถ้าเทียบจากตาราง Pattern Letters and Symbols ในหัวข้อที่แล้ว เราจะเจอว่า y เนี่ยมันคือ year-of-era ส่วน Y มันเป็น week-based-year ซึ่งในส่วนของ Presentation ก็มีความหมายว่า year เหมือนกัน และถ้ามองดูดีๆ จะเห็นว่าเรามี u ด้วยซึ่งมีความหมายว่า year เหมือนกัน

ส่วนที่ยิ่งประหลาดไปอีกคือ Example ครับ จะเห็นว่า u กับ y เนี่ยเหมือนกันเลยคือ 2004; 04 แต่ว่า Y เนี่ยดันเป็น 1996; 96 อะไรของ Java วะคับ อะไรคือ year-of-era อะไรคือ week-based-year แล้วมันต่างจากปีที่เรารู้จักกันยังไง ผมจะสรุปสั้นๆ ให้ฟัง

Year-of-era (y เล็ก หรือ u)

ตัวเนี่ยไม่มีอะไรซับซ้อนเลยครับ มันคือปีอย่างที่เรารู้จักกันนี่แหละ หรือถ้าทางเทคนิคหน่อยคือปีในทาง Gregorian calendar ที่เราใช้กันอยู่เป็นปกติหรือ ค.ศ. นั่นเองครับ ถ้าเทียบกับปีปัจจุบันคือ 2021

Week-based-year (Y ใหญ่)

ตัวนี้แหละที่เป็นสาเหตุให้ผมต้องเขียนบล็อกนี้ขึ้นมา Week-based-year คือการนับปีตามสัปดาห์ครับ ซึ่งเราก็น่าจะเอะต่อว่า แล้วสัปดาห์นี่มันเริ่มนับยังไงนะ บางปฏิทินก็นับวันอาทิตย์ บางก็นับวันจันทร์ ในส่วนของ Java นี้นับปฏิทินเริ่มที่วันจันทร์ครับตามมาตรฐาน ISO 8601

Screen Shot 2564-12-01 at 17.32.01.png

ถ้ายังงงอยู่ งั้นลองดูตัวอย่างนี้นะครับ (จริงๆ App Calendar ของ Apple นี่มันก็แอบสปอยอยู่นะ) ถ้าเราใช้ YYYY (Week-based-year) ตอนเรา Format date ในวันที่ 31 ธันวาคม 2021 แทนที่มันจะออกมาเป็น 2021-12-31 สิ่งที่คืนกลับมาจาก DateFormatter จะกลายเป็น 2022-12-31 ครับ เพราะว่าในสัปดาห์นี้ตั้งแต่วันจันทร์ที่ 27 ธันวาคม 2021

แต่ละสัปดาห์เริ่มนับวันจันทร์จริงเหรอ?

พอยิ่งขุดลึกลงไปอีก ผมเลยเจอว่าการที่จะนับแต่ละสัปดาห์จะเริ่มวันจันทร์หรือวันอาทิตย์ ขึ้นอยู่กับ Locale ที่เราใช้ในการ Format ครับโดยเราสามารถใส่หรือไม่ใส่ก็ได้เป็น arguments ตัวที่สองของ DateTimeFormatter.ofPattern() ซึ่ง default Locale จะเป็น Locale.US ซึ่งนับวันอาทิตย์เป็นวันเริ่มต้นของสัปดาห์ครับ ทำให้ในเคสข้างบนของเรามันจะนับ YYYY เป็นปี 2022 ตั้งแต่วันอาทิตย์ที่ 26 ธันวาคม 2021 เลย

แต่ถ้าเราอยากให้นับสัปดาห์เริ่มที่วันจันทร์เราสามารถเปลี่ยนไปใช้ Locale อื่นได้ครับ อย่างเช่น Locale.FRANCE จะเริ่มในวันจันทรแทน ซึ่งจะทำให้เคสวันอาทิตย์ที่ 26 ธันวาคม 2021 ยังคงเป็นปี 2021 อยู่ครับ

แล้วเราควรจะใช้ตัวไหนดี u, y, Y

ถ้าตอบแบบ Consult คงต้องตอบว่า It’s depends แต่ว่าผมสรุปให้คร่าวๆ ครับ

u (year)

ผมเข้าใจว่าในทาง Meaning ตัวนี้น่าจะเหมาะกับเราที่สุดครับ เพราะมันเป็นปีตรงๆ โดยไม่มีอะไร Constraint เลย แต่ผมเข้าใจที่หลายคนไม่เลือกใช้ เพราะมันน่าจะงงๆ เวลาไปเขียนร่วมกับหน่วยอื่นๆ คนเลยนิยม y มากกว่า

y (year-of-era)

ตัวนี้คนน่าจะใช้เยอะสุด แล้วน่าจะสับสนน้อยสุด เพราะเราชินกับมันมาจากหลายๆ ภาษา และโดยตัวมันเองก็สื่อออกมาด้วย แต่ตามที่มัน Design มามันถูกออกแบบให้ใช้คู่กับตัว G (era) ด้วยครับ แต่ถ้าเราไม่ใช้ผมว่าก็ไม่ผิดอะไรนะ ส่วนตัวผม Prefer ตัวนี้นะ

Y (week-based-year)

ตัวนี้เหมาะกับ calculation อะไรก็ตามที่เราต้องทำเป็น week-based ครับ ซึ่งอาจจะใช้กับงานที่เป็น batch รันรายสัปดาห์แล้วเก็บแค่ปีกับสัปดาห์

ก็ประมาณนี้ครับ คิดว่าใครที่อ่านมาถึงตรงนี้น่าจะได้อะไรติดไม้ติดมือกลับไปบ้าง ผมกำลังสงสัยว่าภาษาอื่นๆ น่าจะมีเรื่องนี้เหมือนกัน ซึ่งต้องบอกว่า เวลา เนี่ยเป็นหนึ่งในเรื่องที่ยากของ Computer เลยครับและถ้าเราไม่ระวัง เราจะเจอบั๊กแปลกๆ ที่ชอบมาในเวลาแปลกๆ อย่างสิ้นปีได้ ถ้าชอบบล็อกแนวนี้ก็ฝากกดไลค์ กดแชร์ หรือคอมเมนต์ด้วยนะครับ แล้วเจอกันใหม่บล็อกหน้าครับ

Reference