DateTime ถือว่าเป็นเรื่องหนึ่งที่นักพัฒนามักจะได้เจออยู่บ่อย ๆ รวมไปถึงตัวเจ้าของบล็อกเอง แต่ที่ตลกร้ายก็คือเป็นคำสั่งที่ชอบลืมทุกครั้ง!! ดังนั้นบทความนี้จึงขอรวบรวมคำสั่งที่เกี่ยวข้องกับ DateTime ใน Java 8 ละกัน อย่างน้อยเวลาเจ้าของบล็อกลืมก็จะได้กลับมาดูบทความตัวเองได้เลย

สำหรับ Android

โดยปกติบนแอนดรอยด์จะเรียกใช้งาน Java 8 Date/Time API ทันทีไม่ได้ และมี 2 วิธีเพื่อให้สามารถใช้งานได้

ใช้ Instant, LocalDateTime, OffsetDateTime หรือ ZonedDateTime ดี?

ไม่ว่าจะเป็นแบบใดก็ตาม แต่ละแบบมีคุณสมบัติและจุดประสงค์ในการใช้งานที่แตกต่างกัน

  • Instant : เก็บค่าเวลาแบบ UTC+0
  • LocalDateTime : เก็บค่าเวลาโดยอิงจาก Timezone ภายในเครื่อง
  • OffsetDateTime : เก็บค่าเวลาโดยอิงจาก Timezone ภายในเครื่อง โดยมี UTC Offset ด้วย
  • ZonedDateTime : เก็บค่าเวลาโดยอิงจาก Timezone ภายในเครื่อง โดยมี UTC Offset และ Zone ID ด้วย

ในกรณีที่ต้องการเอาค่าเวลามาเทียบกันโดยไม่สนใจเรื่อง Timezone ก็สามารถใช้ Instant ได้เลย แต่ถ้าต้องการแสดงข้อมูลโดยอ้างอิงจาก Timezone ด้วย การใช้ LocalDateTime, OffsetDateTime หรือ ZonedDateTime ก็เป็นตัวเลือกที่ดีกว่า ขึ้นอยู่กับว่าต้องการข้อมูลแบบไหนไปใช้งานบ้าง

Instant.now()
// 2022-02-18T00:08:40.642

LocalDateTime.now()
// 2022-02-18T00:15:40.642

OffsetDateTime.now()
// 2022-02-18T00:15:40.642

ZonedDateTime.now()
// 2022-02-18T00:15:40.643+07:00[Asia/Bangkok]

จุดแตกต่างอย่างนึงของ Instant คือการแปลงเวลาเป็น Epoch Millisecond ได้ ในขณะที่ตัวอื่นที่แปลงได้แค่ Epoch Second

// Instant
val instant: Instant = /* ... */
val epochSecond = instant.epochSecond
val epochMilli = intent.toEpochMilli()

สร้าง DateTime จาก String

สามารถสร้าง DateTime ใด ๆ จาก String โดยใช้ Standard Pattern หรือ Custom Pattern ก็ได้

val formatter = DateTimeFormatter.ofPattern("d MMM yyyy hh:mm:ss a")
val dateTime = LocalDateTime.parse("9 Feb 2022 09:30:12 AM", formatter)
// 2022-02-09T09:30:12

val formatter = DateTimeFormatter.ISO_DATE_TIME
val dateTime = ZonedDateTime.parse("2022-02-09T09:30:12+07:00[Asia/Bangkok]", formatter)
// 2022-02-09T09:30:12+07:00[Asia/Bangkok]

ดึงข้อมูลบางส่วนใน DateTime ไปใช้งาน

ข้อมูลที่เป็น DateTime จะอยู่ในรูปของ ChronoField เสมอ ดังนั้นจะเรียกผ่าน Getter Method ของค่าแต่ละตัวก็ได้ หรือจะกำหนด Field ที่ต้องการก็ได้เช่นกัน

val dateTime: LocalDateTime = /* ... */
// 2022-02-18T10:15:40.642

val hour: Int = dateTime.hour
// 10

val month: Month = dateTime.month
// FEBRUARY

val month: Int = dateTime.get(ChronoField.HOUR_OF_DAY)
// 10

val month: Int = dateTime.get(ChronoField.MONTH_OF_YEAR)
// 2

แก้ไขข้อมูลใน DateTime

val dateTime: LocalDateTime = /* ... */
// 2022-02-19T22:06:05.663

val newDateTime = dateTime.withYear(2020)
    .withDayOfMonth(15)
    .withHour(10)
// 2020-02-15T10:06:05.663
การแก้ไขข้อมูลใด ๆ จะเป็นการสร้างข้อมูลตัวใหม่ขึ้นมาเนื่องจาก DateTime ใน Java 8 ถูกออกแบบมาให้เป็น Immutable เสมอ

การแสดงข้อมูลมูลในรูปแบบของ String

โดยปกติแล้วไม่ว่าจะเป็น Instant, LocalDateTime, OffsetDateTime หรือ ZonedDateTime จะแสดงข้อมูลในรูปของ ISO 8601 ให้อยู่แล้ว ถ้าต้องการแสดงในรูปแบบอื่น ๆ สามารถใช้คำสั่ง format แล้วกำหนด DateTimeFormatter ได้ตามใจชอบเลย

val dateTime: LocalDateTime = /* ... */
val formatter = DateTimeFormatter.ofPattern("dd MMM yyyy, HH:mm")
val text = dateTime.format(formatter)
// 19 Feb 2022, 19:36

แสดงวันที่ภาษาไทย

DateTimeFormatter รองรับการกำหนด Locale ได้ ดังนั้นถ้าอยากให้แสดงเป็นภาษาไทย ก็ให้กำหนด Locale เป็นภาษาไทยเพิ่มเข้าไปด้วย

val dateTime: LocalDateTime = /* ... */
val formatter = DateTimeFormatter.ofPattern("dd MMM yyyy, HH:mm")
    .withLocale(Locale("th"))
val text = dateTime.format(formatter)
// 19 ก.พ. 2022, 19:36

แสดงวันที่แบบพุทธศักราช

โดยปกติแล้วการใช้ DateTime จะอยู่ในรูปของคริสต์ศักราช แต่บางครั้งก็ต้องการแสดงผลให้อยู่ในรูปของพุทธศักราช ให้กำหนด Chronology เป็น ThaiBuddhistChronology ใน DateTimeFormatter ได้เลย

val dateTime: LocalDateTime = /* ... */
val time: LocalTime = /* ... */
val formatter = DateTimeFormatter.ofPattern("EEEE dd MMMM Gyyyy, HH:mm")
    .withLocale(Locale("th"))
    .withChronology(ThaiBuddhistChronology.INSTANCE)
val text = dateTime.format(formatter)
// วันเสาร์ 19 กุมภาพันธ์ พ.ศ.2565, 16:15
ถ้าใช้ ThreeTenABP จะมีปัญหาว่าแสดง Era เป็น ค.ศ. เสมอ
ISO8601 ไม่รองรับการแสดง น. ต่อท้าย จึงต้องเพิ่มเข้าไปเองเมื่อต้องการให้แสดงต่อท้ายเวลา

แปลงวันที่แบบพุทธศักราชให้เป็นคริสต์ศักราช

ในกรณีที่ต้องการใช้งานแค่วันที่ สามารถแปลง DateTime ใด ๆ ให้เป็น ThaiBuddhistDate แบบนี้ได้เลย

val local: LocalDateTime = /* ... */
// 2022-02-19T21:26:02.316

val thai: ThaiBuddhistDate = ThaiBuddhistDate.from(local)
// ThaiBuddhist BE 2565-02-19

แต่ถ้าอยากได้เวลาด้วย ให้ใช้คำสั่ง atTime(...) เพิ่มเข้าไป เพื่อทำเป็น ChronoLocalDateTime แทน

val local: LocalDateTime = /* ... */
val thai: ChronoLocalDateTime<ThaiBuddhistDate> = ThaiBuddhistDate.from(local)
    .atTime(local.toLocalTime())

แต่ในกรณีที่รับข้อมูลแบบ String เข้ามาแล้วต้องแปลงให้เป็นคริสต์ศักราช ก็สามารถใช้ DateTimeFormatter เข้ามาช่วยได้เช่นกัน

val input = "19/02/2565"
val formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
    .withChronology(ThaiBuddhistChronology.INSTANCE)
val inputDate = ChronoLocalDate.from(LocalDate.parse(input, formatter))
// 2022-02-19

การกำหนด Chronology ใน DateTimeFormatter จะช่วยแปลงข้อมูลให้กลายเป็นคริสต์ศักราชโดยทันที

ข้อมูล DateTime ควรใช้ในรูปของคริสต์ศักราช ส่วนพุทธศักราชควรใช้แค่ตอนแสดงผลข้อมูลเท่านั้น ไม่ควรนำมาใช้ในการคำนวณโดยตรง
ถ้าใช้ ThreeTenABP จะมีปัญหาการแปลงข้อมูลจากพุทธศักราชเป็นคริสศักราชในบางกรณี ดังนั้นควรเช็คให้ดี

แปลงค่าระหว่าง Instant, LocalDateTime, OffsetDateTime และ ZonedDateTime

// Instant
val instant: Instant = /* ... */
val local: LocalDateTime = LocalDateTime.ofInstant(dateTime, ZoneOffset.of("+07:00"))
val offset: OffsetDateTime = OffsetDateTime.ofInstant(dateTime, ZoneOffset.of("+02:00"))
val zoned: ZonedDateTime = ZonedDateTime.ofInstant(dateTime, ZoneId.of("Asia/Bangkok"))
/*
instant = 2022-02-18T20:27:42.208Z
local   = 2022-02-19T03:27:42.208
offset  = 2022-02-18T22:27:42.208+02:00
zoned   = 2022-02-19T03:27:42.208+07:00[Asia/Bangkok]
*/

// LocalDateTime
val local: LocalDateTime = /* ... */
val instant: Instant = local.toInstant(ZoneOffset.ofHours(10))
val offset: OffsetDateTime = local.atOffset(ZoneOffset.of("+02:00"))
val zoned: ZonedDateTime = local.atZone(ZoneId.of("Asia/Bangkok"))
/* 
local   = 2022-02-19T03:30:54.812
instant = 2022-02-18T17:30:54.812Z
offset  = 2022-02-19T03:30:54.812+02:00
zoned   = 2022-02-19T03:30:54.812+07:00[Asia/Bangkok]
*/

// OffsetDateTime
val offset: OffsetDateTime = /* ... */
val instant: Instant = offset.toInstant()
val local: LocalDateTime = offset.toLocalDateTime()
val zoned: ZonedDateTime = offset.toZonedDateTime()
/*  
offset  = 2022-02-19T03:33:36.232+07:00
instant = 2022-02-18T20:33:36.232Z
local   = 2022-02-19T03:33:36.232
zoned   = 2022-02-19T03:33:36.232+07:00
*/

// ZonedDateTime
val zoned: ZonedDateTime = /* ... */
val instant: Instant = zoned.toInstant()
val local: LocalDateTime = zoned.toLocalDateTime()
val offset: OffsetDateTime = zoned.toOffsetDateTime()
/*
zoned   = 2022-02-19T03:35:23.022+07:00[Asia/Bangkok]
instant = 2022-02-19T03:35:23.022+07:00[Asia/Bangkok]
local   = 2022-02-19T03:35:23.022
offset  = 2022-02-19T03:35:23.022+07:00
*/

การแปลง OffsetDateTime เป็น ZonedDateTime จะทำได้ Zone ID ที่มีค่าเป็น null ถ้าต้องการเพิ่ม Zone ID เข้าไปทีหลังทำให้แบบนี้

val offset: OffsetDateTime = /* ... */
val zoned: ZonedDateTime = offset.toZonedDateTime().withZoneSameLocal(ZoneId.of("Asia/Bangkok"))
/*
offset = 2022-02-18T00:43:30.826+07:00
zoned  = 2022-02-18T00:43:30.826+07:00[Asia/Bangkok]
*/

การกำหนด Zone ID ให้กับ ZonedDateTime

ZonedDateTime.now()
ZonedDateTime.now(ZoneId.systemDefault())
ZonedDateTime.now(ZoneId.of("Asia/Bangkok"))
ZonedDateTime.now(ZoneOffset.of("+02:00"))
ZonedDateTime.now(ZoneOffset.ofHours(-8))
ZonedDateTime.now(ZoneOffset.ofHoursMinutes(7, 30))
ZoneOffset เป็น Subclass ของ ZoneId

LocalDate และ LocalTime

ในกรณีที่ต้องการใช้งานแค่วันที่ (Date) หรือเวลา (Time) สามารถใช้ LocalDate หรือ LocalTime ได้เลย

// Date
LocalDate.now()
LocalDate.of(2022, 03, 21)
LocalDate.of(2022, Month.MARCH, 21)
LocalDate.parse("2022.03.21", DateTimeFormatter.ofPattern("yyyy.MM.dd"))

// Time
LocalTime.now()
LocalTime.of(10, 45, 30)
LocalTime.parse("10:45:30", DateTimeFormatter.ofPattern("HH:mm:ss"))

หรือจะแปลงจาก DateTime ก็ได้เช่นกัน

// Date
val offset: OffsetDateTime = /* ... */
LocalDate.from(offset)

// Time
val zoned: ZonedDateTime = /* ... */
LocalTime.from(zoned)

แปลง LocalDate และ LocalTime จาก [Local/Offset/Zoned]DateTime

val dateTime: LocalDateTime = /* ... */
val localDate: LocalDate = dateTime.toLocalDate()
val localTime: LocalTime = dateTime.toLocalTime()
/*
dateTime  = 2022-02-18T01:05:13.685
localDate = 2022-02-18
localTime = 01:05:13.685
*/

แปลง ZoneDateTime เป็น Zone ID หรือ Zone Offset อื่น

ในกรณีที่มี ZoneDateTime อยู่แล้ว และต้องการแปลงเป็น Zone ID หรือ Zone Offset อื่นๆ ก็ใช้คำสั่งแบบนี้ได้เลย

val isoDateTime = "2024-12-15T14:27:15+07:00"
val original = ZonedDateTime.parse(isoDateTime, DateTimeFormatter.ISO_DATE_TIME)
val withId = original.withZoneSameInstant(ZoneId.of("Asia/Bangkok"))
val withOffset = original.withZoneSameInstant(ZoneOffset.ofHours(+9))

OffsetTime สำหรับ OffsetDateTime

สำหรับ OffsetDateTime จะแตกต่างจากชาวบ้านนิดหน่อยตรงที่สามารถแปลงเป็น OffsetTime ได้ด้วย​ (แต่ ZonedDateTime ทำไม่ได้ซะงั้น)

val dateTime: OffsetDateTime = /* ... */
val offsetTime: OffsetTime = dateTime.toOffsetTime()
/* 
dateTime  = 2022-02-18T01:05:13.685
offsetTime = 01:05:13.685+07:00
*/

โดยใน OffsetTime จะมีทั้ง Time และ UTC Offset ให้ใช้งาน

แปลงเวลาตาม UTC Offset

เหมาะกับกรณีที่ต้องการแปลงเวลาตาม Timezone ที่ต่างกัน

val bangkok: OffsetDateTime = /* ... */
// 2022-02-19T03:51:09.645+07:00

val tokyo: OffsetDateTime = offset.withOffsetSameInstant(ZoneOffset.ofHours(+9))
// 2022-02-19T05:51:09.645+09:00

เพิ่ม/ลด วันและเวลา

// Plus
val offset: OffsetDateTime = /* ... */
// 2022-02-19T15:21:02.434+07:00

val next2Days = date.plusDays(2)
// 2022-02-21T15:21:02.434+07:00

// Minus
val offset: OffsetDateTime = /* ... */
// 2022-02-19T15:23:09.393+07:00

val previous2Months = date.minusMonths(2)
// 2021-12-19T15:23:09.393+07:00

สามารถใช้กับ LocalDate และ LocalTime ได้เช่นกัน

อยู่ในช่วงเวลาที่กำหนดหรือไม่

เนื่องจาก LocalTime จะมีแค่คำสั่ง isBefore กับ isAfter ให้ใช้ ไม่ได้มีคำสั่งสำหรับเช็คว่าอยู่ในช่วงเวลาที่กำหนดหรือป่าว ดังนั้นต้องใช้ทั้ง 2 Method รวมกันเพื่อสร้างเป็น Extension Function เอาไว้ใช้งานเอง

fun LocalTime.isInBetween(start: LocalTime, end: LocalTime) =
    if (start.isAfter(end)) this.isAfter(start) || this.isBefore(end)
    else this.isAfter(start) && this.isBefore(end)

อยู่ในช่วงวันที่กำหนดหรือไม่

เนื่องจาก LocalDate จะมีแค่คำสั่ง isBefore กับ isAfter ให้ใช้ ดังนั้นต้องใช้ทั้ง 2 Method รวมกันเพื่อสร้างเป็น Extension Function เอาไว้ใช้งานเอง

fun LocalDate.isInBetween(start: LocalDate, end: LocalDate) = this.isAfter(start) && this.isBefore(end)

วันและเวลาห่างกันเท่าไร

อยากจะเช็คว่าห่างกันกี่วัน, กี่ชั่วโมง​ หรือกี่นาที ก็สามารถเช็คผ่าน ChronoUnit ได้เลย

val dateTimeA: LocalDateTime = /* ... */
val dateTimeB: LocalDateTime = /* ... */
val diffInHour = ChronoUnit.HOURS.between(dateTimeA, dateTimeB)
/*
dateTimeA  = 2022-02-01T09:30
dateTimeB  = 2022-02-05T20:00
diffInHour = 106
*/

เช็ค Leap Year

สำหรับ Leap Year จะเช็คได้ใน LocalDate, Year และ YearMonth ดังนั้นถ้าข้อมูลเป็น Instant, LocalDateTime, OffsetDateTime หรือ ZonedDateTime ก็ให้แปลงข้อมูลให้อยู่ในรูปของคลาสเหล่านี้ก่อน

val dateTime: LocalDateTime = /* ... */
// 2022-02-19T21:45:53.784+07:00

val isLeapYear = LocalDate.from(dateTime).isLeapYear
// false

val isLeapYear = Year.from(date).isLeap
// false

val isLeapYear = YearMonth.from(now).isLeapYear
// false

หาจำนวนวันในเดือนนั้น ๆ

เหมาะกับกรณีที่ข้อมูลเดือนเป็น Dynamic จึงไม่รู้ว่าข้อมูลจะเป็นของเดือนไหน แต่ต้องการรู้ว่าวันสุดท้ายของเดือนนั้นคือวันที่เท่าไร

val month = 3
val daysInMonth = Month.of(month).length(true)
// 31

val year = 2022
val month = 2
YearMonth.of(year, month).lengthOfMonth()
// 28
ถ้าใช้ YearMonth จะเช็คเรื่อง Leap Year ให้โดยอัตโนมัติ แต่ถ้าเป็น Month จะต้องกำหนดเองว่าเป็น Leap Year หรือไม่