นากการ์ตูนสีมิ้นต์หมุนปุ่มทองเหลืองบนปฏิทินตั้งโต๊ะแบบวงล้อคู่ที่เชื่อมกัน ข้างแจกันดอกบัวและถ้วยชาบนโต๊ะ

Java แสดงวันที่เป็น พ.ศ. แบบไม่ต้อง +543 เอง: ThaiBuddhistDate และกับดัก withLocale

น้องกายEngineering

ระบบไทยแทบทุกระบบต้องเจอ พ.ศ. ใบเสร็จ เอกสารราชการ หน้าโปรไฟล์ วิธีที่เห็นบ่อยคือเอาปีมา + 543 ตอนแสดง แล้ว - 543 ตอนรับค่ากลับ ใช้ได้ครับ แต่พอมีหลายจุดในโค้ด ก็จะมีสักจุดที่ลืมบวก หรือบวกซ้ำสองรอบ จนได้ปี 3112

Java มีปฏิทินพุทธศักราชในตัวมาตั้งแต่ java.time (Java 8) คือ ThaiBuddhistDate วันนี้ผมจะใช้มันแสดงและรับวันที่ พ.ศ. แบบไม่ต้องบวกเลขเอง

WHY: ทำไมไม่บวก 543 เอง

  • การบวกเลขกระจายอยู่หลายที่ ทำให้พลาดง่าย ทั้งลืมบวกและบวกซ้ำ
  • ถ้าเก็บปี พ.ศ. ลงฐานข้อมูลปนกับ ค.ศ. วันหนึ่งจะแยกไม่ออกว่าแถวไหนเป็นปีอะไร
  • ปีอธิกสุรทิน (กุมภาพันธ์มี 29 วัน) ต้องคิดจากปี ค.ศ. ถ้าแปลงผิดจังหวะ การตรวจวันที่จะผิดตาม

หลักที่ผมใช้คือ เก็บและคำนวณเป็น LocalDate (ค.ศ.) เสมอ แปลงเป็น พ.ศ. แค่ตอนแสดงผลและตอนรับค่าจากผู้ใช้

HOW: เครื่องมือ 3 ชิ้น

  1. ThaiBuddhistDate.from(localDate) แปลงวันที่ ค.ศ. เป็นวันที่ พ.ศ. วันเดียวกัน
  2. DateTimeFormatter.ofPattern(...).withChronology(ThaiBuddhistChronology.INSTANCE) สั่งให้ formatter ใช้ปฏิทินพุทธ ปีที่พิมพ์ออกมาจึงเป็น พ.ศ. และตอน parse ก็อ่านปีเป็น พ.ศ.
  3. ใส่ 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