โปรแกรมเมอร์ กับเรื่องวุ่น ๆ ของวันที่และเวลา

Tinnapat Chaipanich
KBTG Life
Published in
7 min readJun 6, 2024
Ref: joke — A perfect date for a Programmer !! — devRant

ผมมีความคิดที่จะเขียนบทความตอนนี้ตั้งแต่ช่วงต้นเดือนมีนาคมที่ผ่านมา เป็นช่วงที่เพิ่งผ่านพ้นวันที่ 29 กุมภาพันธ์​ ซึ่งเป็นวันที่มีความพิเศษ​มาไม่นาน และผมก็ได้ทราบข่าวจากบรรดาเว็บไซต์ที่รายงานข่าวไอทีว่าในวันนั้นได้เกิดเหตุการณ์ระบบคอมพิวเตอร์ทำงานผิดพลาดขึ้นหลายแห่งในโลก เลยเกิดความคิดขึ้นมาว่าในการเขียนโปรแกรม เราต้องมีการทำงานเกี่ยวกับวันที่และเวลา ผมเองก็มีประสบการณ์ในอดีตหลายครั้งที่เขียนโปรแกรมเกี่ยวกับวันที่และเวลา แล้วเจอปัญหาทำงานผิดพลาดจากหลายสาเหตุ ตั้งแต่เรื่องเล็ก ๆ อย่าง Time Zone เรื่องปี พ.ศ.​ กับ ค.ศ.​ หรือเรื่องที่ซับซ้อนขึ้นมาหน่อย อย่างเรื่อง Leap Year หรือเหตุการณ์ในประวัติศาสตร์ก็ยังมี ดังนั้นผมจะมาเล่าถึงปัญหาต่าง ๆ จากที่เคยพบเจอมาครับ

Leap Year หรือปีอธิกสุรทิน

เดือนกุมภาพันธ์ที่ผ่านมาของปีนี้ นับว่ามีความพิเศษ ที่ 4 ปีจะมีสักครั้งหนึ่ง นั่นคือมี 29 วัน แทนที่จะเป็น 28 วันเหมือนปีส่วนใหญ่

เท่ากับว่าปีนี้เป็นปีอธิกสุรทิน หรือภาษาอังกฤษใช้คำว่า Leap Year คือเป็นปีที่มีการเพิ่มหนึ่งวัน เป็นวันที่ 29 กุมภาพันธ์ ที่ต้องมีปีอธิกสุรทินก็เนื่องจากเราเทียบเวลาว่าเวลาที่โลกใช้โคจรรอบดวงอาทิตย์ครบ 1 รอบนับเป็น 1 ปี หรือ 365 วัน แต่จริง ๆ แล้ว การที่โลกโคจรรอบดวงอาทิตย์ครบ 1 รอบจะใช้เวลา 365 วันกับอีกประมาณ 6 ชั่วโมง หรือ 365.25 วัน ดังนั้นเพื่อไม่ให้ความผิดเพี้ยนมันมากขึ้นเรื่อย ๆ ทุก ๆ 4 ปี เราจึงต้องเพิ่มวันเข้าไปอีก 1 วัน ดังนั้นสูตรการคำนวณปีอธิกสุรทินเงื่อนไขแรก คือเป็นปีที่หารด้วย 4 ลงตัวนั่นเอง แต่การชดเชยเพียงเท่านี้ ก็ยังไม่แม่นยำ 100% เพราะว่าโลกเราโคจรรอบดวงอาทิตย์ครบ 1 รอบ จริง ๆ แล้วก็ไม่ได้เท่ากับ 365.25 วันพอดีอีก แต่มันคือประมาณ 365.2425 วัน ขาดจาก 1/4 ไปหน่อยนึง ดังนั้นถ้าชดเชยบวกไป 1 วันในทุก ๆ 4 ปี มันก็จะเป็นการชดเชยที่มากเกินไป จึงมีเงื่อนไขเพิ่มเติมเข้าไปอีก ว่าปีค.ศ. ที่หารด้วย 100 ลงตัว ก็จะถือว่าไม่ใช่ปีอธิกสุรทิน คือไม่ต้องชดเชย (เช่น ปี 1900) แต่ก็มียกเว้นอีกชั้นว่าถ้าปีไหนหารด้วย 400 ลงตัวจะถือเป็นปีอธิกสุรทิน (เช่น ปี 2000)

Logic ที่ถูกต้องในการเขียนโปรแกรมตรวจสอบ Leap Year จึงเขียนได้แบบนี้ครับ

public static boolean isLeapYear(int year) {
// A year is a leap year if it is divisible by 4,
// except for years that are divisible by 100,
// unless it is also divisible by 400.

// Check if the year is divisible by 400
if (year % 400 == 0) {
return true;
}

// Check if the year is divisible by 100
if (year % 100 == 0) {
return false;
}

// Check if the year is divisible by 4
if (year % 4 == 0) {
return true;
}

// If none of the above conditions are met, it's not a leap year
return false;
}

ปัญหาอันดับแรกของ Leap Year เลย คือการเขียนสูตรคำนวณ Leap Year ผิดครับ ที่มักผิดบ่อย ๆ คือเขียนแค่นี้

// Check if the year is divisible by 4
if (year % 4 == 0) {
return true;
}

ซึ่งไม่ถูกต้อง สูตรจริง ๆ ต้องเขียนตามด้านบนครับ

แต่จริง ๆ แล้วในการเขียนโปรแกรมเรื่องนี้ไม่ค่อยมีปัญหาเท่าไหร่ หรือเรียกว่าไม่เป็นปัญหาเลยดีกว่า เพราะในชีวิตจริง เราไม่ค่อยมีความจำเป็นต้องเขียนโปรแกรมเพื่อตรวจสอบว่าปีนี้เป็น Leap Year หรือเปล่าสักเท่าไหร่ เนื่องจากในแต่ละภาษาจะมี Standard Library ที่ช่วยตรงนี้ให้ และ Library เหล่านี้จะรู้จักว่าปีไหนเป็น Leap Year อยู่แล้ว

แต่สาเหตุที่ทำให้โปรแกรมคอมพิวเตอร์ทำงานผิดพลาดโดยไม่คาดคิดใน Leap Year นั้น จะเป็นเรื่องของการใช้ Library ต่าง ๆ เหล่านี้โดยไม่มีความเข้าใจ หรือนำข้อมูลที่เป็นผลลัพธ์จากการใช้ Library ไปใช้ที่ไม่ถูกต้องมากกว่า

Leap Year Problem

เนื่องจาก Leap Year มีจำนวนวันที่ต่างจากปีทั่ว ๆ ไป จึงมีโปรแกรมคอมพิวเตอร์จำนวนไม่น้อยที่มีความผิดพลาดใน Business Logic ของโปรแกรม ไม่ว่าจะเป็นการคำนวณว่าปีไหนเป็น Leap Year หรือมี Logic การจัดการวันที่ที่ไม่ได้คำนึงถึง Leap Year ที่ต้องมีการจัดการแตกต่างออกไปจากปีอื่น ๆ ปัญหานี้เราเรียกว่า Leap Year Problem ซึ่งเป็นปัญหาที่ค่อนข้างจะ Common คือเจอกันมาตลอดในทุก ๆ Leap Year สำหรับในปี 2024 นี้ก็เช่นกัน มีระบบคอมพิวเตอร์จำนวนมากทำงานผิดพลาด กระทบไปถึงตลาดเกม PlayStation ที่มีรายงานว่ามีบางเกมเล่นไม่ได้ในวันที่ 29 กุมภาพันธ์ของปีนี้ ซึ่งถ้าเพื่อน ๆ สนใจอยากทราบ ก็มีคนรวบรวมข้อมูลไว้ให้ ณ ที่นี้แล้วครับ

การจัดการวันที่และเวลาในภาษา Java

ก่อนที่เราจะมาดูกันต่อว่าปัญหาคืออะไร เราต้องทำความเข้าใจก่อน ว่า Library ในแต่ละภาษามีการจัดการวันที่อย่างไร มีการเก็บข้อมูลอย่างไร สำหรับในภาษา Java จะมี Library อยู่ 2 ตัวหลัก ๆ ในการจัดการวันที่และเวลา คือ

  1. java.util.Date เป็น Library ที่มีมาตั้งแต่ในยุคเริ่มต้นของภาษา Java โดยหลักการของ Library ตัวนี้จะเก็บค่าเป็น Epoch Time หรือเป็นค่ามิลลิวินาที ที่สัมพันธ์กับเวลา ณ​ วันที่ 1 มกราคม ปี ค.ศ. 1970 เวลา 0:00:00
  2. java.time Package เป็น Library ใหม่ที่เพิ่มเข้ามาใน Java 8 ซึ่งแก้ไขข้อด้อยของ java.util.Date ใช้งานง่ายขึ้น มีประสิทธิภาพดีขึ้น และลดโอกาสที่จะเกิดบัคจากการเขียนโปรแกรม มี Class ที่ใช้ในการเก็บข้อมูลวันที่และเวลาหลายแบบให้เลือกใช้ตามความต้องการ เช่น ต้องการวันที่อย่างเดียว เวลาอย่างเดียว หรือทั้งวันที่และเวลา และต้องการเก็บค่า Time Zone ด้วยมั้ย เป็นต้น ดังนั้นโค้ดที่เขียนขึ้นใหม่ในสมัยนี้ควรจะใช้ java.time Package เป็นหลักแล้วครับ

ในบทความนี้ผมจะยกตัวอย่างโดยใช้ java.util.Date เป็นหลัก เนื่องจากปัญหาส่วนใหญ่ที่เกิดขึ้นก็เกิดจากโค้ดเก่า ๆ ที่ยังใช้ Package ตัวนี้อยู่แหละครับ

สำหรับการใช้งาน java.time Package ผมจะไม่ได้กล่าวถึงในบทความนี้ ถ้าเพื่อน ๆ สนใจ เบื้องต้นสามารถศึกษาได้จากเว็บไซต์นี้ครับ

การแปลงค่าจาก String เป็น Object ที่ใช้เก็บค่าวันที่และเวลา

ในการแปลงค่าจาก String ไปเป็น java.util.Date นั้นเราจะใช้ Class ชื่อ SimpleDateFormat โดยในการใช้งาน เราจะต้องระบุรูปแบบของวันที่ที่จะใส่เป็น Input เข้าไปตอน New จากนั้นก็ใช้คำสั่ง .parse เช่น

DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date date = df.parse("2024-01-01");
System.out.println(date);

จะได้ผลลัพธ์ดังนี้

Mon Jan 01 00:00:00 ICT 2024

ในการแปลงค่าจาก String เป็น Object นั้นสามารถเลือกได้ว่าจะประมวลผลรูปแบบข้อมูล (Parse) ในแบบ เคร่งครัด (Strict) หรือ ผ่อนผัน (Lenient) โดยค่าเริ่มต้นจะเป็นแบบผ่อนผัน นั่นคือเราสามารถระบุค่าวันเดือนปีที่ไม่มีอยู่จริงก็ได้ โดยผลลัพธ์ที่ได้จะถูกทดวันเข้าไป ตัวอย่างเช่น เราใส่วันที่เป็นวันที่ 32 เดือนมกราคม คือเกินวันที่มีจริงคือวันที่ 31 ไป 1 วัน ผลลัพธ์ที่ได้จะกลายเป็นวันที่ 1 เดือนกุมภาพันธ์ เป็นต้น

DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
System.out.println(df.parse("2024-01-35")); // Sun Feb 04 00:00:00 ICT 2024
System.out.println(df.parse("2024-15-01")); // Sat Mar 01 00:00:00 ICT 2025

จะเห็นว่า 2024–01–35 ได้เป็นวันที่ 4 กุมภาพันธ์ (เกินวันจริงไป 4 วัน) และ 2024–15–01 จะได้เป็นวันที่ 1 มีนาคม 2025 (เกินเดือนจริงไป 3 เดือน)

ถ้าเราต้องการให้โปรแกรมประมวลผลรูปแบบข้อมูลแบบเคร่งครัด สามารถทำได้โดยเรียก method .setLenient(false)

DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
df.setLenient(false);
System.out.println(df.parse("2024-01-35"));

หลังจากเรา setLenient เป็นค่า False แล้ว เมื่อเราระบุค่า Input เป็น 2024–01–35 โปรแกรมจะฟ้อง Error ว่าเป็นข้อมูลวันที่ที่ไม่ถูกต้อง (เป็นวันที่ที่ไม่มีอยู่จริง)

java.text.ParseException: Unparseable date: "2024-01-35"

หลังจากที่เรารู้หลักการของ Package java.util.Date กันไปแล้ว ลองมาดูกันว่ามีกรณีไหนบ้างที่การใช้งาน Package นี้อาจทำให้โปรแกรมได้ผลลัพธ์ที่ไม่ถูกต้อง

ปัญหา Precision Lost

จากที่กล่าวไปในตอนต้นว่าการเก็บข้อมูลของ java.util.Date จะเก็บเป็นมิลลิวินาที เทียบกับวันที่ 1 มกราคม ค.ศ.​ 1970 ดังนั้นมันจะมีข้อมูลทั้งวัน เดือน ปี และเวลา เก็บอยู่ในตัวเสมอ ซึ่งเป็นสิ่งที่ต้องคอยระลึกไว้ แม้ว่าเราไม่ได้ใช้ข้อมูลทั้งหมดก็ตาม เช่น ใช้เฉพาะวันที่ ไม่ใช้เวลา หรือใช้เฉพาะวันที่กับเดือน ไม่ได้ใช้ข้อมูลปี เป็นต้น

สมมุติว่าผมต้องการเขียนโปรแกรม เพื่ออ่านค่าวันและเดือนจากหน้าจอ UI มาใช้งานต่อ โดยไม่ใช้ปี (สมมุติหน้าจอ UI มีให้กรอกแค่ 2 ช่อง คือวันและเดือนเท่านั้น)

TimeZone.setDefault(TimeZone.getTimeZone("Asia/Bangkok"));
DateFormat df = new SimpleDateFormat("ddMM");

Calendar calendar = new GregorianCalendar();
calendar.setTime(df.parse("2902"));
System.out.println("date: " + calendar.get(Calendar.DATE));
System.out.println("month: " + (calendar.get(Calendar.MONTH) + 1));

แล้วผมรันโปรแกรมนี้ในปี ค.ศ.​ 2024 ซึ่งเป็นปีที่มีวันที่ 29 กุมภาพันธ์ ผลลัพธ์คือ

date: 1
month: 3

จะเห็นว่ามันไม่ถูกต้อง เนื่องจากการเก็บข้อมูลของ java.util.Date อ้างอิงจาก Epoch Time ดังนั้นเวลาเราแปลงข้อมูล “2902” ที่รับเข้ามาจากหน้าจอ UI ไปเป็น Epoch Time ซึ่งเป็นข้อมูลในระดับมิลลิวินาที ตัว Library จะใช้ปี 1970 เป็นค่าเริ่มต้น ทำให้ค่า Epoch Time ที่ได้เป็นค่าของวันที่ 29 เดือนกุมภาพันธ์ ปี 1970 ซึ่งเดือนกุมภาพันธ์ปี 1970 นั้นมีแค่ 28 วัน วันที่ 29 กุมภาพันธ์จึงถูกทดไปเป็นวันที่ 1 มีนาคมนั่นเอง​

ปัญหา พ.ศ. และ ค.ศ.

แน่นอนครับ ปี ค.ศ. และ พ.ศ. ไม่เท่ากันครับ ปกติแล้วเวลาเราใช้ Library ต่าง ๆ มักจะใช้เป็นปี ค.ศ. แต่บางครั้งเรารับ Input มาจากหน้าจอเป็นปี พ.ศ.​ ถ้าเราเอาข้อมูลมาใช้งานต่อโดยตรง โดยไม่ได้แปลงเป็นปี ค.ศ. ก่อน ก็จะเกิดความผิดพลาดได้ครับ

DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
df.setLenient(false);
System.out.println(df.parse("2567-02-29")); // java.text.ParseException: Unparseable date: "2567-02-29"

Daylight Saving Time

ก่อนอื่นเรามาทำความรู้จักกับ Daylight Saving Time หรือที่เรียกกันสั้นๆ ว่า DST กันก่อน เพราะประเทศไทยไม่มีแบบนี้

DST เป็นการปรับเปลี่ยนเวลาตามฤดูกาลในบางประเทศ โดยมีวัตถุประสงค์หลักเพื่อประหยัดพลังงานและใช้ประโยชน์จากแสงธรรมชาติในช่วงเวลากลางวันให้ได้มากที่สุด

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

เราลองเลือกเล่นกับข้อมูลของเมืองที่มี Daylight Saving Time สักเมืองนึงดีกว่า สมมุติเอาเป็น New York แล้วกัน จากที่ค้นหาในอินเทอร์เน็ต ได้ข้อมูลการปรับเวลา Daylight Saving Time ของ New York ดังนี้

ข้อมูลจากเว็บไซต์ https://www.timeanddate.com/time/zone/usa/new-york

ความหมายคือเมื่อถึงเวลา 02:00 ใน Time Zone EST นาฬิกาวันที่ 12 มีนาคม ค.ศ. 2023 ของ New York จะถูกปรับขึ้นหน้าให้เร็วขึ้น 1 ชม.​ จากเวลา ตี 2 ไปเป็น ตี 3 ซึ่ง Time Zone ที่อยู่ในช่วง Daylight Saving Time นี้จะเรียกว่า EDT

TimeZone.setDefault(TimeZone.getTimeZone("America/New_York"));

// Date and time before DST transition
Calendar calendar = Calendar.getInstance();
calendar.set(2023, Calendar.MARCH, 12, 1, 0, 0); // March 12, 2023, 1:00 AM
Date beforeDST = calendar.getTime();
System.out.println("Before DST: " + beforeDST);

// Add 1 hour to the date
calendar.add(Calendar.HOUR_OF_DAY, 1);
Date afterDST = calendar.getTime();
System.out.println("After adding 1 hour: " + afterDST);

ผลลัพธ์ที่ได้ คือ

Before DST: Sun Mar 12 01:00:00 EST 2023
After adding 1 hour: Sun Mar 12 03:00:00 EDT 2023

จะเห็นว่ามันก็ถูกต้องแหละครับ ถ้าเราดูข้อมูลทั้ง วันที่ เวลา และ Time Zone ประกอบกันทั้งหมด แต่ปัญหาจะเกิดในกรณีที่เราไม่ได้ใช้ข้อมูลทุกองค์ประกอบมารวมร่างกัน เช่น เอาเฉพาะวันที่มาใช้

สมมุติว่าผมเขียนฟังก์ชันสำหรับคำนวณว่าวันพรุ่งนี้เป็นวันที่เท่าไหร่ โดยเทียบกับวันที่ ณ ขณะนี้ เช่น สมมุติว่าตอนนี้เป็นวันที่ 12 มีนาคม ค.ศ. 2023 เวลา 5 ทุ่มครึ่ง โดยใช้ลอจิกว่า ให้บวกเวลาเข้าไป 24 ชั่วโมง

TimeZone.setDefault(TimeZone.getTimeZone("America/New_York"));
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date now = df.parse("2023-03-11 23:30:00"); // Assume this is current time now
System.out.println("Before DST: " + beforeDST);

// Add 1 days to the date
long oneDay = 24 * 60 * 60 * 1000; // Number of milliseconds in a day
Date nextDay = new Date(now.getTime() + oneDay);
System.out.println("Date after 1 day: " + nextDay);

ผลลัพธ์ที่ได้ คือ

Before DST: Sat Mar 11 23:30:00 EST 2023
Date after 1 day: Mon Mar 13 00:30:00 EDT 2023

คำตอบนี้ถูกต้องก็จริง เพราะพอบวกจากเวลาตั้งต้นไป 24 ชั่วโมงมันก็เข้าช่วง Daylight Saving Time พอดีที่ต้องบวกเวลาไปอีก 1 ชั่วโมง แต่ถ้าเราต้องการเอาเฉพาะวันที่จากผลลัพธ์ไปใช้ โดยไม่เอา เวลา และ Time Zone ไปด้วย ก็อาจจะไม่ตรงกับความต้องการ เพราะถ้าคิดว่าวันนี้เป็นวันที่ 11 โดยไม่สนใจว่าเป็นเวลาเท่าไหร่ แล้วเราต้องการเขียนโปรแกรมคำนวณว่า อีก 1 วันถัดมาหรือวันพรุ่งนี้เป็นวันที่เท่าไหร่ คำตอบควรจะเป็นวันที่ 12 ไม่ใช่วันที่ 13

เวลาที่หายไปของประเทศไทย

เนื่องจากประเทศไทยไม่มี Daylight Saving Time คือไม่มีการที่เราต้องมาคอยปรับเวลาขึ้นหน้าถอยหลังทุกปี ก็น่าจะไม่มีความซับซ้อนเรื่องการคำนวณบวกลบวันที่ แล้วได้เวลาไม่ตรงเหมือนตัวอย่างข้างบนใช่ไหมครับ ผมก็คิดอย่างนั้น แต่วันหนึ่งผมก็ได้พบบัคกับระบบหนึ่งจนได้ จำได้ว่าเป็นปัญหากับข้อมูลวันเดือนปีเกิดของลูกค้าที่อายุมากแล้วรายหนึ่ง ที่เมื่อโปรแกรมมีการทำงานกับข้อมูลของลูกค้ารายนี้แล้ว ทำให้โปรแกรมทำงานผิดพลาด

อันนี้ไม่ใช่โค้ดจริง แต่เป็นโค้ดเพื่อจำลองสถานการณ์เฉย ๆ นะครับ สมมุติว่า Logic ของโปรแกรมนี้เป็นการรับข้อมูลวันที่ แล้วเราต้องการแปลงเป็น java.util.Date Object เพื่อนำไปทำงานต่อ อยากให้เพื่อน ๆ ลองดูว่าเห็นสิ่งผิดปกติในโค้ดนี้ไหมครับ…

TimeZone.setDefault(TimeZone.getTimeZone("Asia/Bangkok"));        
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
df.setLenient(false);

Date date = df.parse("1920-04-01 00:00:00"); // Assume this send from UI or other programs
System.out.println(date);

ดูเผิน ๆ เหมือนโค้ดนี้ไม่น่ามีปัญหาใช่ไหมครับ แต่ถ้าลองรัน จะได้ Error แบบนี้ครับ

java.text.ParseException: Unparseable date: "1920-04-01 00:00:00"

ลองเปลี่ยนเวลาเป็นเที่ยงคืน 10 นาที ดู

TimeZone.setDefault(TimeZone.getTimeZone("Asia/Bangkok"));
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
df.setLenient(false);

Date date = df.parse("1920-04-01 00:10:00");
System.out.println(date);

ก็ยัง Error อยู่เหมือนเดิม

java.text.ParseException: Unparseable date: "1920-04-01 00:10:00"

ทีนี้ผมลองเปลี่ยนเวลาเป็นเที่ยงคืนครึ่งบ้าง

TimeZone.setDefault(TimeZone.getTimeZone("Asia/Bangkok"));
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
df.setLenient(false);

Date date = df.parse("1920-04-01 00:30:00");
System.out.println(date);

ผลลัพธ์ที่ได้คือ

Thu Apr 01 00:30:00 ICT 1920

มันผ่านนะครับ อ้าว แล้วเที่ยงคืนตรง กับเที่ยงคืน 10 นาทีมันผิดยังไง

ผมต้องไปค้นหาข้อมูลในอินเทอร์เน็ต จนได้ความรู้ว่าก่อนหน้าวันที่ 1 เมษายน พ.ศ. 2463 (หรือปี ค.ศ.​ 1920)​ ประเทศไทยไม่ได้ใช้ Time Zone ที่ GMT+7:00 แต่เป็น GMT+6:42:04 จากนั้นในสมัยพระบาทสมเด็จพระมงกุฎเกล้าเจ้าอยู่หัว มีพระบรมราชโองการโปรดเกล้าโปรดกระหม่อมให้ตั้งแต่วันที่ 1 เมษายน พ.ศ. 2463 ประเทศไทยทำการตั้งเวลาเร็วขึ้น 17 นาที 56 วินาที เพื่อให้เวลามาตรฐานอยู่ที่ GMT+7:00 จนถึงปัจจุบัน

ข้อมูลจาก Wikipedia

มันหมายความว่ายังไง ลองดูตัวอย่าง

TimeZone.setDefault(TimeZone.getTimeZone("Asia/Bangkok"));

DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
Date date = df.parse("1920-03-31 23:59:59.999");
System.out.println("date: " + date);
long timeMs = date.getTime();
System.out.println("timeMs: " + timeMs);

long newTimeMs = timeMs + 1;
Date newDate = new Date(newTimeMs);
System.out.println("newDate: " + newDate);
System.out.println("timeMs: " + newTimeMs);

เพื่อน ๆ เดาผลลัพธ์ของการรันโปรแกรมตัวนี้ได้ไหมครับ

ผลลัพธ์ที่ได้คือ

date: Wed Mar 31 23:59:59 ICT 1920
timeMs: -1570084924001

newDate: Thu Apr 01 00:17:56 ICT 1920
timeMs: -1570084924000

จะเห็นว่าเราเริ่มต้นการเซ็ตค่าวันที่ เป็นวันที่ 31 มีนาคม ค.ศ. 1920 เวลา 23 นาฬิกา 59 นาที 59 วินาที และ 999 มิลลิวินาที จากนั้นทำการบวกเวลาเพิ่มเข้าไป 1 มิลลิวินาที ผลลัพธ์ที่ได้คือ วันที่และเวลากลับกลายเป็น 1 เมษายน ค.ศ. 1920 เวลา 0 นาฬิกา 17 นาที 56 วินาที

นั่นคือสำหรับประเทศไทย ในวันที่ 1 เมษายน พ.ศ. 2463 ไม่มีเวลาตั้งแต่เที่ยงคืนตรง หรือ 0 นาฬิกา 0 นาที 0 วินาที ไปจนถึง 0 นาฬิกา 17 นาที 55 วินาที เวลาช่วงนี้เป็นเวลาของประเทศไทยที่ไม่มีอยู่จริงบนโลกใบนี้

คือต้องบังเอิญแค่ไหนที่มีการบันทึกข้อมูลลูกค้าที่เกิดวันที่ 1 เมษายน พ.ศ. 2463 พอดี แต่ก็ต้องขอบคุณเคสนี้ ที่ทำให้ผมต้องลองผิดลองถูกจนเข้าใจว่าประเทศไทยเรามันมีเวลาที่หายไปในอดีตด้วย

คำแนะนำในการหลีกเลี่ยงปัญหา

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

  1. ก่อนใช้งาน Library ใด ๆ ในการจัดการข้อมูลวันที่และเวลา ต้องศึกษาหลักการและคุณลักษณะของ Library ตัวนั้นให้เข้าใจก่อนนำมาใช้งาน
  2. สำหรับภาษา Java แนะนำให้ใช้ Package java.time.* ในการจัดการเรื่องวันที่และเวลา เนื่องจากเป็น Library ที่ออกแบบใหม่ให้เข้าใจง่ายขึ้น ใช้งานง่ายขึ้น และลดโอกาสที่จะเกิดบักจากการที่โปรแกรมเมอร์นำไปเขียนไม่ถูกวิธี
  3. ทำ Unit Test ให้ครอบคลุม ถ้าโปรแกรมของเรามีการรับค่าเข้ามาเป็นวันที่ ก็ต้องมีการทดสอบ Edge Case หรือเคสที่เป็น Input แปลก ๆ ของข้อมูลวันที่ด้วย ซึ่ง Leap Year ก็คือว่าเป็น Edge Case นึงที่ต้องทดสอบให้ครบครับ
  4. ถ้าต้องมีการส่งข้อมูลวันที่และเวลาระหว่างระบบและเป็นข้อมูลที่มีความสำคัญ ให้ใช้รูปแบบที่เป็น Standard Format เช่น ISO8601 และควรจะต้องมีการส่ง Time Zone หรือ Time Offset (เวลา + หรือ — จากเวลา GMT) เข้ามาด้วย ถ้าไม่มีก็ต้องมีการกำหนดใน Specification ให้เข้าใจตรงกันว่าใช้เวลาใน Time Zone ใด หรือใช้เวลามาตรฐาน GMT เป็นต้น

สำหรับบทความในวันนี้ ผมขอจบเพียงเท่านี้นะครับ ถ้าเพื่อน ๆ เคยมีประสบการณ์ เขียนโปรแกรม แล้วเจอ Error หรือเจอบัคแปลก ๆ ก็สามารถนำมาแลกเปลี่ยนกันใน Comment ได้นะครับ แล้วพบกันใหม่ตอนหน้า ซึ่งจะเป็นเรื่องอะไรผมก็ยังไม่ทราบนะครับ :P

โปรแกรมของเรา มักจะมีอายุยืนยาวกว่าที่เราคิด (ref: Lazy programming : r/ProgrammerHumor)

สุดท้ายขอทิ้งท้ายด้วยข้อคิด ว่าปัญหาเกี่ยวกับเรื่องการจัดการวันที่และเวลานั้นมักจะไม่เกิดให้เห็นทันทีที่ระบบ Go-Live แต่จะเกิดในอนาคตหลายปีต่อมา และโปรแกรมคอมพิวเตอร์ที่เราเขียนมักจะมีอายุยืนยาวกว่าที่เราคิดครับ ดังนั้นอย่ามั่นใจ 100% ว่าโปรแกรมที่เราเขียนมันอยู่ไม่ถึงวันที่มันจะเกิดปัญหา ซึ่งก็ไม่แน่เสมอไปนะครับ :) สวัสดีครับ

สำหรับใครที่ชื่นชอบบทความนี้ อย่าลืมกดติดตาม Medium: KBTG Life เรามีสาระความรู้และเรื่องราวดีๆ จากชาว KBTG พร้อมเสิร์ฟให้ที่นี่ที่แรก

--

--

Tinnapat Chaipanich
KBTG Life

DEVelopment eXcellence engineer — DEVX@KBTG / Console Gamer