
Java แสดงวันที่เป็น พ.ศ. แบบไม่ต้อง +543 เอง: ThaiBuddhistDate และกับดัก withLocale
ระบบไทยแทบทุกระบบต้องเจอ พ.ศ. ใบเสร็จ เอกสารราชการ หน้าโปรไฟล์ วิธีที่เห็นบ่อยคือเอาปีมา + 543 ตอนแสดง แล้ว - 543 ตอนรับค่ากลับ ใช้ได้ครับ แต่พอมีหลายจุดในโค้ด ก็จะมีสักจุดที่ลืมบวก หรือบวกซ้ำสองรอบ จนได้ปี 3112
Java มีปฏิทินพุทธศักราชในตัวมาตั้งแต่ java.time (Java 8) คือ ThaiBuddhistDate วันนี้ผมจะใช้มันแสดงและรับวันที่ พ.ศ. แบบไม่ต้องบวกเลขเอง
WHY: ทำไมไม่บวก 543 เอง
- การบวกเลขกระจายอยู่หลายที่ ทำให้พลาดง่าย ทั้งลืมบวกและบวกซ้ำ
- ถ้าเก็บปี พ.ศ. ลงฐานข้อมูลปนกับ ค.ศ. วันหนึ่งจะแยกไม่ออกว่าแถวไหนเป็นปีอะไร
- ปีอธิกสุรทิน (กุมภาพันธ์มี 29 วัน) ต้องคิดจากปี ค.ศ. ถ้าแปลงผิดจังหวะ การตรวจวันที่จะผิดตาม
หลักที่ผมใช้คือ เก็บและคำนวณเป็น LocalDate (ค.ศ.) เสมอ แปลงเป็น พ.ศ. แค่ตอนแสดงผลและตอนรับค่าจากผู้ใช้
HOW: เครื่องมือ 3 ชิ้น
ThaiBuddhistDate.from(localDate)แปลงวันที่ ค.ศ. เป็นวันที่ พ.ศ. วันเดียวกันDateTimeFormatter.ofPattern(...).withChronology(ThaiBuddhistChronology.INSTANCE)สั่งให้ formatter ใช้ปฏิทินพุทธ ปีที่พิมพ์ออกมาจึงเป็น พ.ศ. และตอน parse ก็อ่านปีเป็น พ.ศ.- ใส่
Locale.forLanguageTag("th-TH")เพื่อให้ชื่อเดือนเป็นภาษาไทย เช่น ตุลาคม หรือ ต.ค.
ตอนผมรันบน JDK 21 วันที่ 18 ตุลาคม 2026 ได้ผลแบบนี้:
- pattern
d MMMM G yyyyได้18 ตุลาคม พ.ศ. 2569 - pattern
d MMM yyyyได้18 ต.ค. 2569 - pattern
dd/MM/yyyyแล้ว parse"18/10/2569"กลับได้LocalDateเป็น2026-10-18
กับดัก: withLocale ไม่เปลี่ยนปฏิทิน
หลายคนรู้ว่า locale tag เขียนต่อท้ายได้ว่า th-TH-u-ca-buddhist (ca คือ calendar) แล้วคิดว่าใส่ใน withLocale ก็จบ
ผลที่ผมรันจริง:
ofLocalizedDate(FormatStyle.FULL).withLocale(th-TH-u-ca-buddhist)ได้วันอาทิตย์ที่ 18 ตุลาคม ค.ศ. 2026ยังเป็น ค.ศ.ofLocalizedDate(FormatStyle.FULL).localizedBy(th-TH-u-ca-buddhist)ได้วันอาทิตย์ที่ 18 ตุลาคม พ.ศ. 2569
เหตุผลอยู่ใน เอกสาร DateTimeFormatter: localizedBy (มีตั้งแต่ Java 10) จะใช้ส่วนขยาย ca ใน locale ไป override ปฏิทินของ formatter ส่วน withLocale ไม่ทำแบบนั้น ถ้าอยากชัวร์ที่สุด ใช้ withChronology(ThaiBuddhistChronology.INSTANCE) ตรงๆ อ่านแล้วรู้เลยว่าตั้งใจใช้ พ.ศ.
โค้ดพร้อม assertions
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.chrono.ThaiBuddhistChronology;
import java.time.chrono.ThaiBuddhistDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.ChronoField;
import java.util.Locale;
public class Main {
static final Locale TH = Locale.forLanguageTag("th-TH");
static final DateTimeFormatter LONG_BE = DateTimeFormatter
.ofPattern("d MMMM G yyyy", TH)
.withChronology(ThaiBuddhistChronology.INSTANCE);
static final DateTimeFormatter INPUT_BE = DateTimeFormatter
.ofPattern("dd/MM/yyyy")
.withChronology(ThaiBuddhistChronology.INSTANCE);
static String show(LocalDate d) {
return LONG_BE.format(d); // เก็บเป็น ค.ศ. แสดงเป็น พ.ศ.
}
static LocalDate parseFromUser(String text) {
return LocalDate.from(INPUT_BE.parse(text)); // รับ พ.ศ. คืนเป็น ค.ศ.
}
public static void main(String[] args) {
LocalDate d = LocalDate.of(2026, 10, 18);
ThaiBuddhistDate be = ThaiBuddhistDate.from(d);
assert be.get(ChronoField.YEAR) == 2569;
assert ThaiBuddhistDate.of(2569, 10, 18).equals(be);
assert show(d).equals("18 ตุลาคม พ.ศ. 2569");
assert parseFromUser("18/10/2569").equals(d);
// กับดัก: withLocale ไม่เปลี่ยนปฏิทิน แต่ localizedBy เปลี่ยน
Locale thBuddhist = Locale.forLanguageTag("th-TH-u-ca-buddhist");
DateTimeFormatter full = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
assert full.withLocale(thBuddhist).format(d).contains("2026");
assert full.localizedBy(thBuddhist).format(d).contains("พ.ศ. 2569");
// ปีอธิกสุรทินคิดจากปี ค.ศ.: 2567 = 2024 มี 29 ก.พ. ส่วน 2569 = 2026 ไม่มี
assert LocalDate.from(ThaiBuddhistDate.of(2567, 2, 29)).equals(LocalDate.of(2024, 2, 29));
boolean threw = false;
try {
ThaiBuddhistDate.of(2569, 2, 29);
} catch (DateTimeException e) {
threw = true;
}
assert threw;
System.out.println("ok");
}
}
ตอนตรวจบทความ โค้ดนี้คอมไพล์ด้วย javac และรันด้วย java -ea (เปิด assertions) บน JDK 21 ของเครื่องนี้
ข้อสังเกต: ข้อความชื่อเดือนภาษาไทยมาจากข้อมูล locale (CLDR) ที่มากับ JDK ถ้าต่างเวอร์ชัน JDK รูปแบบเล็กๆ อาจต่างกันได้ เทสต์ในระบบจริงควรเทียบกับ JDK ที่ใช้ deploy
ใช้ตรงไหนในงานจริง
| งาน | วิธี |
|---|---|
| แสดงวันที่บนใบเสร็จ/เอกสาร | LONG_BE.format(localDate) |
| ช่องกรอกวันเกิดแบบ พ.ศ. | parseFromUser("dd/MM/yyyy") แล้วเก็บเป็น LocalDate |
| API ส่งข้อมูลให้ระบบอื่น | ส่ง ISO 2026-10-18 (ค.ศ.) เสมอ ให้ฝั่งแสดงผลแปลงเอง |
| รายงานรายปีงบประมาณ | คำนวณบน LocalDate แล้วค่อยแปลงปีตอนแสดง |
Recap
- เก็บและคำนวณวันที่เป็น
LocalDate(ค.ศ.) เสมอ - แสดงผล พ.ศ. ด้วย formatter ที่
withChronology(ThaiBuddhistChronology.INSTANCE) - ใส่
Locale.forLanguageTag("th-TH")เพื่อชื่อเดือนภาษาไทย - ถ้าใช้ locale
th-TH-u-ca-buddhistให้ใช้localizedByไม่ใช่withLocale - parse ข้อความ พ.ศ. จากผู้ใช้ด้วย formatter เดียวกัน แล้วแปลงกลับเป็น
LocalDate - รันโค้ดด้านบนด้วย
java -eaเพื่อเปิด assertions