Java Date & Time

Phai Panda
6 min readAug 12, 2022

--

นักพัฒนาหลายคนที่เพิ่งจับภาษา Java อาจมีความคิดเหมือนกับผมที่ว่า Date และ Time ในภาษาจาวานี่มันยังไงกันแน่ ทำไมมีให้เลือกเยอะ นอกจาก make it work ตาม Stack Overflow แล้วจริงๆมันเป็นอย่างไรกัน

ไม่เน้นลึกลับ

เครื่องคอมฯของผมติดตั้ง JDK 8 ไว้อ้างอิงเนื้อหาในบทความนี้

openjdk version “1.8.0_322”
OpenJDK Runtime Environment (build 1.8.0_322-b06)
OpenJDK 64-Bit Server VM (build 25.322-b06, mixed mode)

มาเริ่มกันเลย

java.util.Date

Date today = new Date();
System.out.println(today);

ผลลัพธ์

Fri Aug 12 12:18:48 ICT 2022

อ่านได้ว่า ศุกร์ สิงหาคม วันที่ 12 เวลา 12 นาฬิกา 18 นาที 48 วินาที ตามเวลาอินโดจีน ปี ค.ศ. 2022

ICT คือ Indochina Time แปลว่า เวลาอินโดจีน ประกอบไปด้วยประเทศ กัมพูชา, ลาว, ไทยและเวียดนาม

พวกเรารู้ใช่ไหมว่าเวลาบนโลกของเราไม่เท่ากัน สมมติอยู่ประเทศไทยคือเที่ยงวันทันใดที่เปิดประตูไบฟรอสต์ (Bifrost) มุดไปอีกประเทศหนึ่งอาจเป็นเที่ยงคืนก็ได้ สิ่งนี้เรียกว่า Time zone กำหนดตามตำแหน่งที่ตั้งของประเทศนั้นๆบนโลกใบนี้

ภาพต่อไปนี้แสดงตำแหน่งของประเทศไทย ณ กรุงเทพมหานคร

ภาพจาก Time Zone Map

จากภาพ แท่งสีเทาพาดจากเหนือจรดใต้ เล่าว่าเวลานี้แสดงที่ Time zone +7

หรือพูดง่ายๆว่า ICT หรือเวลาอินโดจีนนั้นมี Time zone +7 นั่นเอง

จากภาพ หากเพื่อนๆเลือกจุดสีแดงใดๆก็จะพบกับตัวย่ออื่นๆนอกเหนือจาก ICT เช่น MMT, PHST (PHT), CST เหล่านี้คือกลุ่มประเทศต่างๆอ้างอิงตามที่ตั้งซึ่งส่งผลต่อเวลาให้บวกเพิ่มขึ้นหรือลดลงก็ได้ ผมขอเรียกตัวย่อเหล่านี้ว่า Local time zone หรือเวลาท้องถิ่น

ตารางต่อไปนี้ได้ตัดมาบางส่วนเพื่อแสดงชื่อเต็มของเวลาท้องถิ่นดังกล่าว

ภาพจาก World Time Server

มีอีกมาตราฐานชื่อ UTC ย่อมาจาก Universal Time Co-ordinated เกิดจากการอ้างอิงเวลามาตรฐานกรีนิช (ภาษาอังกฤษคือ Greenwich Mean Time ย่อ GMT) อ่านเพิ่มเติม

GMT เป็นเวลาของ ลองจิจูด ที่ 0° ที่ตัดผ่านหอดูดาวหลวงกรีนิชในกรีนิช ลอนดอน สหราชอาณาจักร ค่าเวลานี้วัดโดยการจับเวลาตามแสงอาทิตย์ (คลาสสิกมาก)

เมื่อ UTC พัฒนาจาก GMT และกำหนดเป็นเวลามาตรฐาน ดังนั้นเวลาท้องถิ่น (Local time zone) จึงเร็วกว่าหรือช้ากว่าเวลามาตรฐานที่เมืองกรีนิชแตกต่างกันไป

ปัจจุบันกรมอุทกศาสตร์ กองทัพเรือ เป็นผู้ควบคุมเวลามาตรฐานของประเทศไทย ซึ่งกำหนดเวลามาตรฐานของประเทศเป็น UTC +7 ชั่วโมง (แสดงการเทียบ)

นั่นชี้ให้เห็นว่าค่าเวลา Time zone เทียบกับ UTC นี้แต่ละประเทศสามารถกำหนดได้เองขึ้นอยู่กับความเหมาะสมและการค้าเป็นหลัก (การเดินเรือ) หมายถึงเปลี่ยนแปลงในอนาคตได้

ไม่น่าเชื่อว่า Date today = new Date(); จะทรงพลังถึงเพียงนี้ ฮ่า

กลับมาที่ภาษา Java คำถามคือ new Date() ได้วันเวลานี้มาจากไหน? คำตอบคือ จากเครื่องคอมฯที่ติดตั้ง JRE ครับ

เมื่อใดก็ตามที่เรา new Date() ระบบปฏิบัติการของเรา (OS) จะส่งค่าวันเวลาให้กับ JRE สิ่งนี้เกิดขึ้นเพราะคำสั่ง System.currentTimeMillis() ซึ่งอยู่ใน constructor ของคลาส Date ถูกทำงาน

public static native long currentTimeMillis();

Returns the current time in milliseconds. Note that while the unit of time of the return value is a millisecond, the granularity of the value depends on the underlying operating system and may be larger.

java.util.GregorianCalendar

คลาส GregorianCalendar สืบทอดจากคลาส java.util.Calendar เมื่อสร้าง GregorianCalendar ก็จะไปเรียก constructor ของ Calendar ดังลำดับต่อไปนี้

GregorianCalendar()
GregorianCalendar(TimeZone zone, Locale aLocale)
Calendar(TimeZone zone, Locale aLocale) //สร้างออบเจ็กต์นี้ก่อน

โดยที่ค่าตัวแปร zone และ aLocale ได้มาจาก TimeZone.getDefaultRef() และ Locale.getDefault(Locale.Category.FORMAT) ตามลำดับ นี่แสดงว่าการจะสร้างออบเจ็กต์ Calendar ขึ้นมาได้จะต้องให้ค่าของ TimeZone และ Locale ก่อนเสมอและจากที่สังเกตเพื่อนๆก็จะทราบว่า java.util.Date ไม่มีเรื่องนี้เลย

หรือพูดง่ายๆว่า java.util.Date ไม่สามารถกำหนด Time zone เองได้

ทราบอย่างนี้เราก็ควรใช้ java.util.Calendar แทน java.util.Date เสียเลย

หมายเหตุ

  • TimeZone คือ java.util.TimeZone
  • Locale คือ java.util.Locale

กลับมาที่ GregorianCalendar ความพิเศษของมันคือค่าของวันเวลาตามปฏิทินกริกอเรียน ประกาศใช้โดยสมเด็จพระสันตะปาปาเกรกอรีที่ 13

พูดได้ว่านั่นวันเวลาเมืองฝรั่งเขา แล้วเมืองพุทธอย่างไทยเราล่ะ?

org.joda.time.chrono.BuddhistChronology

Joda-Time หนึ่งในไลบรารีจาวาที่มียอดดาวน์โหลดอย่างมากโดยเฉพาะก่อน Java เวอร์ชัน 1.8 หรือ 8 จะ release เพื่อจัดการกับวันเวลาให้ง่ายขึ้นซึ่งมีการเรียกร้องให้รวมกับ java.time (JSR-310) อีกด้วย

คลาส org.joda.time.chrono.BuddhistChronology ของ Joda-Time คือ Buddhist calendar system หรือระบบปฏิทินชาวพุทธ ต่างจากปฏิทินเกรกอเรียนเฉพาะในปีและยุคเท่านั้น โดยมีการชดเชย 543 วัน (บวกเพิ่ม) จากปีเกรกอเรียนปัจจุบัน

วิธีการใช้งาน

DateTime today = new DateTime(BuddhistChronology.getInstance());
System.out.println(today);

ผลลัพธ์

2565–08–12T22:13:00.524+07:00

อ่านได้ว่า ปี พ.ศ. 2565 เดือน 8 วันที่ 12 เวลา 22 นาฬิกา 13 นาที 0 วินาที กับอีก 524 มิลลิวินาที ที่ UTC +7 ส่วนตัว T นั้นนำมาคั่นระหว่างวันและเวลาตามมาตราฐาน ISO 8601

A single point in time can be represented by concatenating a complete date expression, the letter “T” as a delimiter, and a valid time expression. For example, “2007–04–05T14:30”

หมายเหตุ

  • DateTime คือ org.joda.time.DateTime

java.util.Calendar

พิมพ์คำสั่งต่อไปนี้แล้วรัน

Calendar c = Calendar.getInstance();
Date today = c.getTime();
String timeZoneName = c.getTimeZone().getDisplayName();
String timeZoneId = c.getTimeZone().getID();
System.out.println(today);
System.out.println(timeZoneName);
System.out.println(timeZoneId);

ผลลัพธ์

Fri Aug 12 23:16:19 ICT 2022
Indochina Time
Asia/Bangkok

เหตุเพราะ Calendar(TimeZone zone, Locale aLocale) ซึ่งอยู่ใน Calendar.getInstance() ถูกเรียกให้ทำงาน โดยค่าของตัวแปร zone และ aLocale ได้มาจาก TimeZone.getDefault() และ Locale.getDefault(Locale.Category.FORMAT) ตามลำดับ

เราจึงสามารถกำหนด Time zone และ Locale ได้เอง เช่น

TimeZone timeZone = TimeZone.getTimeZone("Asia/Bangkok");
Locale locale = Locale.US;
Calendar c = Calendar.getInstance(timeZone, locale);

จะได้ผลลัพธ์เหมือนกับตัวอย่างก่อนหน้า

Fri Aug 12 23:16:19 ICT 2022
Indochina Time
Asia/Bangkok

ลองเปลี่ยน Time zone ไปที่โตเกี่ยวประเทศญี่ปุ่นกันหน่อย

TimeZone timeZone = TimeZone.getTimeZone("Asia/Tokyo");
Locale locale = Locale.US;
Calendar c = Calendar.getInstance(timeZone, locale);

ผลลัพธ์กลับแปลกประหลาด

Fri Aug 12 23:47:40 ICT 2022
Japan Standard Time
Asia/Tokyo

กล่าวคือค่าของ today ยังคงได้ ICT ทั้งๆที่ timeZoneName และ timeZoneId นั้นถูกต้อง?

สาเหตุเกิดจากคำสั่ง c.getTime() นั้นเรียกไปยัง new Date(getTimeInMillis()) ซึ่ง java.util.Date ไม่มี Time zone (จำได้ใช่ไหม?)

การแก้ไข ให้การกำหนด TimeZone.setDefault ก่อนเรียกคำสั่ง c.getTime() ดังนี้

TimeZone timeZone = TimeZone.getTimeZone("Asia/Tokyo");
Locale locale = Locale.US;
Calendar c = Calendar.getInstance(timeZone, locale);
TimeZone.setDefault(timeZone);
Date today = c.getTime();

ผลลัพธ์

Sat Aug 13 01:47:40 JST 2022
Japan Standard Time
Asia/Tokyo

นั่นเพราะเวลาของ Asia/Bangkok คือ +7 ส่วนของ Asia/Tokyo คือ +9 ดังนั้นที่โตเกียวเวลาต้องเดินเร็วกว่าของกรุงเทพ 2 ชั่วโมง

ญี่ปุ่นจะได้เห็นพระอาทิตย์ขึ้นก่อนไทยเสมอ

สังเกตสิเวลาที่ได้ไม่ว่าจะเป็น ICT หรือ JST ล้วนเป็นตัวย่อของ Local time zone ไม่ใช่ UTC

อยากให้เป็น UTC ต้องทำอย่างไร?

java.text.SimpleDateFormat

เห็นใช้กันบ่อย รู้ไหมคลาสนี้จะต้อง new instance ทุกครั้งในแต่ละ Thread หรือที่เรียกว่า not thread safe

ให้ค่า timeZone เป็น Asia/Bangkok

String pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
SimpleDateFormat f = new SimpleDateFormat(pattern);
String result = f.format(today);
System.out.println(result);

หมายเหตุ ตัวแปร today อยู่ในตัวอย่างก่อนหน้า ได้ค่าจาก c.getTime() เมื่อตัวแปร c คือออบเจ็กต์ Calendar

ผลลัพธ์

Sat Aug 13 00:27:45 ICT 2022
Indochina Time
Asia/Bangkok
2022–08–13T00:27:45.044+0700

ให้ค่า timeZone เป็น Asia/Tokyo

ผลลัพธ์

Sat Aug 13 02:27:45 JST 2022
Japan Standard Time
Asia/Tokyo
2022–08–13T02:27:45.072+0900

java.time.format.DateTimeFormatter

ทำหน้าที่คล้ายกับ SimpleDateFormat แต่ DateTimeFormatter จะไม่สร้าง instance ในแต่ละครั้งที่ใช้ระหว่างการประมวลผลแบบมัลติเธรด เรียกว่า Thread safe

String pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
DateTimeFormatter f = DateTimeFormatter.ofPattern(pattern);
ZonedDateTime z = ZonedDateTime.ofInstant(today.toInstant(), ZoneId.of(timeZoneId));
String result = z.format(f);
System.out.println(result);

ให้ค่า timeZone เป็น Asia/Bangkok

ผลลัพธ์

Sat Aug 13 00:59:40 ICT 2022
Indochina Time
Asia/Bangkok
2022–08–13T00:59:40.260+0700

ให้ค่า timeZone เป็น Asia/Tokyo

ผลลัพธ์

Sat Aug 13 02:59:40 JST 2022
Japan Standard Time
Asia/Tokyo
2022–08–13T02:59:40.388+0900

หมายเหตุ

  • ZoneDateTime คือ java.time.ZonedDateTime
  • ZoneId คือ java.time.ZoneId

Calendar & Locale

ก่อนหน้านี้เราได้กำหนดค่าของตัวแปร locale จาก Locale.US คลาสนี้มีรายละเอียดที่ควรทราบดังนี้ (อ้างอิง)

Locale จะแสดงถึงภูมิศาสตร์ที่เฉพาะเจาะจงทั้งทางด้านการเมืองและวัฒนธรรม กำหนดขึ้นเพื่อปรับแต่งข้อมูล (วันเวลา) ที่เฉพาะเจาะจงมากขึ้น ประกอบด้วย

  • language
  • script
  • country (region)
  • variant
  • extensions

Language

กำหนดโดยมาตราฐาน ISO 639 ประกอบขึ้นจาก 2 ตัวอักษร (alpha-2 language code) หรือ 3 ตัวอักษร (alpha-3 language code) แต่ต้องไม่เกิน 8 ตัวอักษร

เมื่อตรวจพบว่า language code มีทั้ง 2 ตัวอักษรและ 3 ตัวอักษร, ที่ 2 ตัวอักษรจะถูกนำมาใช้งาน

codes ทั้งหมดดูได้ที่ IANA Language Subtag Registry ค้นหาด้วยคีย์ Type: language

Type: language

รูปแบบ [a-zA-Z]{2,8}

การใช้งานไม่คำนึงถึงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ (case insensitive) แต่ Locale จะใช้ตัวพิมพ์เล็กเสมอ

ตัวอย่าง “en” (English), “ja” (Japanese), “kok” (Konkani)

country (region)

กำหนดโดยมาตราฐาน ISO 3166 ประกอบขึ้นจาก alpha-2 country code หรือมาตราฐาน UN M.49 ประกอบขึ้นจาก numeric-3 area code ก็ได้

codes ทั้งหมดดูได้ที่ IANA Language Subtag Registry ค้นหาด้วยคีย์ Type: region

Type: region

รูปแบบ [a-zA-Z]{2} | [0–9]{3}

การใช้งานไม่คำนึงถึงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ (case insensitive) แต่ Locale จะใช้ตัวพิมพ์ใหญ่เสมอ

ตัวอย่าง “US” (United States), “FR” (France), “029” (Caribbean)

variant

จะเป็นค่าใดก็ได้ที่ใช้เพื่อระบุความแตกต่างของ Locale แต่ขอให้คั่นด้วยขีดล่าง (underscore)

codes ทั้งหมดดูได้ที่ IANA Language Subtag Registry ค้นหาด้วยคีย์ Type: variant

Type: variant

รูปแบบ SUBTAG ((‘_’|’-’) SUBTAG)*
เมื่อ SUBTAG คือรูปแบบ [0–9][0–9a-zA-Z]{3} | [0–9a-zA-Z]{5,8}

การใช้งานคำนึงถึงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ (case sensitive)

ตัวอย่าง “polyton” (Polytonic Greek), “POSIX”

คลาส Locale มี constructor 3 แบบ

Locale(String language)
Locale(String language, String country)
Locale(String language, String country, String variant)

มันยังจัดเตรียมค่าคงที่ที่สะดวกจำนวนหนึ่งซึ่งสามารถใช้เพื่อสร้างออบเจ็กต์ Locale สำหรับที่ใช้กันทั่วไป เช่น Locale.US (United States)

ใช้งานคลาส Locale กับคลาส Calendar

ทดสอบเฉพาะ Locale

Locale locale = Locale.US;
Calendar c = Calendar.getInstance(locale);
int year = c.get(Calendar.YEAR);
System.out.println(year);

ผลลัพธ์

2022 //ได้ปีฝรั่ง

ลองเปลี่ยน

Locale locale = new Locale("th", "TH");

ผลลัพธ์

2565 //ได้ปีไทย

ใช้งานคลาส Locale กับคลาส ZonedDateTime

โค้ดตัวอย่างต่อไปนี้จัดเตรียมรูปแบบของวันเวลาเอาไว้ในอาเรย์ของสตริงชื่อ patterns

String[] patterns = {
"yy-MM-dd",
"yyyy-MMM-d",
"E, yyyy-MMMM-d",
"EE, yyyy-MMMM-d",
"EEE, yyyy-MMMM-d",
"EEEE, yyyy-MMMM-dd h:mm:ss a",
"EEEE, yyyy-MMMM-dd kk:mm:ssZ"
};

ที่เลือกใช้ ZonedDateTime เพราะสามารถเข้าใจตัว Z ใน pattern สุดท้ายได้

ถัดไปประกาศให้ Locale มี variant เป็น TH เพื่อแสดงเลขไทย เช่น ๑, ๒, ๓…

Locale locale = new Locale("th", "TH", "TH");

กำหนด Time zone แก่ ZonedDateTime จากค่าวันเวลาปัจจุบัน

ZonedDateTime z = ZonedDateTime.now(ZoneId.of("Asia/Bangkok"));

ลูปอาเรย์ patterns ทั้งหมดด้วย DateTimeFormatter แล้วพิมพ์ result ออกมา

for(String pattern: patterns) {
DateTimeFormatter f = DateTimeFormatter
.ofPattern(pattern, locale)
.withDecimalStyle(DecimalStyle.of(locale))
.withChronology(ThaiBuddhistChronology.INSTANCE);
String result = z.format(f);
System.out.printf("%-30s -> %s%n", pattern, result);
}

เมื่อ

  • เมธอด withChronology เปลี่ยนจากปี ค.ศ. เป็น พ.ศ.
  • %-30s คือจัดชิดซ้าย (เครื่องหมายลบ) และจากซ้ายสุดจองไว้ 30 ตัวอักษร

ผลลัพธ์

yy-MM-dd                       -> ๖๕-๐๘-๑๓
yyyy-MMM-d -> ๒๕๖๕-ส.ค.-๑๓
E, yyyy-MMMM-d -> ส., ๒๕๖๕-สิงหาคม-๑๓
EE, yyyy-MMMM-d -> ส., ๒๕๖๕-สิงหาคม-๑๓
EEE, yyyy-MMMM-d -> ส., ๒๕๖๕-สิงหาคม-๑๓
EEEE, yyyy-MMMM-dd h:mm:ss a -> วันเสาร์, ๒๕๖๕-สิงหาคม-๑๓ ๓:๔๙:๒๑ ก่อนเที่ยง
EEEE, yyyy-MMMM-dd kk:mm:ssZ -> วันเสาร์, ๒๕๖๕-สิงหาคม-๑๓ ๐๓:๔๙:๒๑+0700

นอกเหนือจากที่แสดงให้เห็นในบทความนี้ ยังมีอีกหลายคลาสที่ไม่ได้กล่าวถึง เช่น

  • java.time.LocalDate จัดการวันเท่านั้น
  • java.time.LocalTime จัดการเวลาเท่านั้น
  • java.time.LocalDateTime จัดการทั้งวันและเวลาแต่ไม่รวมโซน (Time zone)
  • java.time.chrono.ThaiBuddhistDate จัดการวันและปีเป็น พ.ศ. แต่ไม่รวมเวลา

สรุป

java.util.Date จะรับ Local time zone จาก JRE ที่ทำงานบนเครื่องคอมฯใดๆและไม่สามารถกำหนด Time zone เองได้ ส่วน Locale มุ่งไปที่การแสดงผลซึ่งจําเพาะเจาะจงตามท้องถิ่นนั้นๆครับ

อ้างอิง

--

--