☕ 5-Min Tech Story #16

0.1 + 0.2 = 0.30000000000000004: หายนะทศนิยมลอยน้ำที่เกือบทำระบบการเงินล่ม

⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 15 วินาที

ลองเปิด Console ในภาษาโปรแกรมมิ่งภาษาไหนก็ได้บนโลก (Java, Kotlin, Python, JS, Go) แล้วสั่งให้มันบวกเลขง่ายๆ อนุบาล 1:

println(0.1 + 0.2 == 0.3) // ผลลัพธ์: false!

สิ่งที่หน้าจอตอบกลับมาไม่ใช่ 0.3 แต่คือ 0.30000000000000004!

คอมพิวเตอร์ที่ฉลาดขนาดคำนวณวงโคจรส่งจรวดไปดาวอังคารได้ ทำไมถึงตกม้าตายกับเลขทศนิยมหลักเดียว? และถ้าคุณเอา Type แบบนี้ไปคำนวณยอดเงินในแอปธนาคาร... เงินของลูกค้าจะเริ่มระเหยหายไปทีละสตางค์อย่างลึกลับ!

🍕 เมื่อพิซซ่าแบ่ง 3 ส่วนไม่ลงตัว: IEEE 754

ในโลกมนุษย์ เราใช้ เลขฐานสิบ เพราะเรามี 10 นิ้วมือ แต่ในโลกคอมพิวเตอร์ ทุกอย่างถูกบีบให้เป็น เลขฐานสอง (0 กับ 1)

ลองนึกภาพว่าคุณสั่งพิซซ่ามา 1 ถาด แล้วบอกให้ตัดแบ่ง 3 ส่วนเท่าๆ กัน... ในระบบเลขฐานสิบ คุณจะได้ 0.333333... วนไปไม่รู้จบ ตัดยังไงก็ไม่มีวันเป๊ะ

สำหรับคอมพิวเตอร์ เลขอย่าง 0.1 หรือ 0.2 ก็เป็นแบบนั้นเป๊ะ!

"0.1 ในเลขฐานสิบ พอแปลงเป็นเลขฐานสอง มันคือ 0.0001100110011... ไม่รู้จบ คอมพิวเตอร์จึงต้อง 'ตัดเศษทิ้ง' เพื่อยัดใส่แรมตามมาตรฐาน IEEE 754"

ดังนั้น ทุกครั้งที่คุณประกาศตัวแปร Double หรือ Float ค่าที่เก็บอยู่ในเครื่องไม่ใช่ค่าจริง แต่เป็น "ค่าประมาณที่ใกล้เคียงที่สุด" เสมอ!

🚀 มหากาพย์ขีปนาวุธแพทริออต (Patriot Missile Failure)

นี่ไม่ใช่แค่บั๊กน่ารักขำขัน... ในปี 1991 ช่วงสงครามอ่าวเปอร์เซีย ระบบป้องกันขีปนาวุธ Patriot ของสหรัฐฯ เกิดความคลาดเคลื่อนในการคำนวณเวลา เพราะการปัดเศษทศนิยมลอยน้ำของเลข 0.1 วินาทีทีละนิดละหน่อย

หลังจากเปิดเครื่องต่อเนื่องไป 100 ชั่วโมง เศษทศนิยมเล็กๆ นั้นสะสมจนเพี้ยนไป 0.34 วินาที... ซึ่งมากพอที่จะทำให้ระบบยิงสกัดขีปนาวุธ Scud ของอิรักพลาดเป้าไปถึง 500 เมตร ส่งผลให้มีทหารเสียชีวิตถึง 28 นาย!

🛡️ ท่ารอดชีวิต: BigDecimal และกฎเหล็ก String Constructor

ในระบบการเงิน (หรืออะไรก็ตามที่ห้ามคลาดเคลื่อนแม้แต่จุดทศนิยมเดียว) เรามีกฎเหล็ก 2 ข้อ:

1. ใช้ BigDecimal เสมอ:

BigDecimal จะไม่เก็บเลขเป็นเลขทศนิยมฐานสอง แต่เก็บเป็นตัวเลขเต็มๆ ก้อนใหญ่ (Unscaled Value) คู่กับตำแหน่งจุดทศนิยม (Scale) ทำให้ไม่มีการปัดเศษมั่วซั่ว

2. กับดักห้ามสร้างด้วย Double!:

// ❌ ผิดมหันต์: โยน Double เข้าไป ก็พังตั้งแต่ต้นทางอยู่ดี
val bad = BigDecimal(0.1)
// ได้ค่าจริง: 0.10000000000000000555111512312578...

// ✅ ถูกต้อง: ส่งเป็น String หรือใช้ BigDecimal.valueOf()
val good = BigDecimal("0.1")
val alsoGood = 0.1.toBigDecimal() // ใน Kotlin

💡 5-Min Takeaway

"Double และ Float มีไว้สำหรับงานกราฟิก เกม และการคำนวณทางวิทยาศาสตร์ที่ยอมรับค่าประมาณได้" แต่ถ้าเป็นระบบบัญชี เงินตรา หรือยอดสต็อกสินค้า ห้ามใช้ Float/Double เด็ดขาด จงใช้ BigDecimal หรือเก็บหน่วยเงินเป็นจำนวนเต็มที่เล็กที่สุด (เช่น เก็บเป็น สตางค์ หรือ Cents ด้วย Type Long) เสมอ