JavaScript, Rounded Number Inaccurate Outcome
- Sathit Jittanupat
- 1 ก.พ.
- ยาว 2 นาที
อัปเดตเมื่อ 3 ก.พ.

สินค้าที่ตั้งราคาตามน้ำหนัก (กิโลกรัม หรือ ตัน) หรือความยาว (เมตร) มักมีปัญหาความคลาดเคลื่อนในการคำนวณมูลค่าสุทธิ เพราะหลายครั้งที่ตัวเลขหน่วยที่ชั่งน้ำหนักหรือวัดความยาวได้มักจะมีทศนิยม 2–3 ตำแหน่ง
ขณะที่ความละเอียดของหน่วยมูลค่าเงินทางการค้าและบัญชีใช้ทศนิยม 2 ตำแหน่ง เมื่อเอาราคาต่อหน่วย (ทศนิยมไม่เกิน 2 ตำแหน่ง) คูณกับ น้ำหนักที่อาจมีทศนิยมได้ถึง 3 ตำแหน่ง มีโอกาสที่ผลลัพธ์กลายเป็นทศนิยม 5 ตำแหน่ง วิธีมาตรฐานที่เราใช้กันคือการปัดเศษแบบ Rounding ให้เหลือเพียง 2 ตำแหน่ง
ยกตัวอย่าง ขายสินค้าราคา 1.7 บาท น้ำหนัก 5.25 กิโลกรัม
ทศนิยม 1 ตำแหน่งคูณกับ 2 ตำแหน่ง
ได้ผลลัพธ์ก่อนปัดเศษเป็น 3 ตำแหน่ง คือ 8.925
และหลังปัดเศษ จำนวนเงินสุทธิเท่ากับ 8.93
JavaScript สามารถเขียนโค้ด คำนวณปัดเศษโดยใช้ Math.round ดังนี้
Math.round(1.7 * 5.25 * 100) / 100แต่ผลลัพธ์จาก JavaScript ให้คำตอบ 8.92 แทนที่จะเป็น 8.93
ที่น่าแปลกใจคือ ไม่ได้เป็นกับทุกตัวเลข เช่น 1.7 คูณกับ 3.25 ได้ 5.525 กลับปัดเศษได้ถูกต้อง
ดูเหมือนเรื่องเล็กน้อย สำหรับคนเขียนโค้ดส่วนใหญ่ก็น่าจะหาทางแก้ไขได้ หรืออาจอธิบายได้ว่าเป็นที่พฤติกรรมของ JavaScript เอง (หวังว่าผู้ใช้จะยอมเข้าใจอย่างที่คุณคิด)

(1)
ทีนี้ลองมาดูเรื่องราวเริ่มต้นของมัน
เหมือนปกติทุกวัน เมื่อผู้ใช้ป้อนข้อมูลรับสินค้าจากต่างประเทศ ใส่ราคาต่อหน่วย, น้ำหนัก และอัตราแลกเปลี่ยน ปรากฏว่าโปรแกรมแสดงทศนิยมมูลค่าสุทธิไม่ใกล้เคียงกับเลขที่จดไว้ในบิล (คิดจากเครื่องคิดเลข)
มนุษย์มักเชื่อว่าคอมพิวเตอร์แม่นยำกว่าตัวเอง จึงพยายามทวน เรียกหลายคนมาลองคำนวณ แต่ไม่มีใครได้คำตอบเหมือนในโปรแกรม
จนแน่ใจว่าเครื่องคิดเลขไม่(น่า)ผิด เรื่องจึงถูกส่งต่อมาเป็นทอดๆ ไม่มีใครหาเจอว่าโปรแกรมพ่นตัวเลขเพี้ยนนั้นออกมาได้อย่างไร แล้วทำไมถึงเกิดกับเฉพาะบิลใบนี้
ราคา 2666.04 ดอลล่าร์
น้ำหนัก 50.375 ตัน
อัตราแลกเปลี่ยน 34.5215
ปกติการคำนวณเช่นนี้โปรแกรมระวังเรื่องการปัดเศษอยู่แล้ว ปัดเศษรอบแรกเมื่อคำนวณมูลค่าสุทธิ แล้วเอามูลค่าสุทธิที่ปัดเศษแล้วไปคูณอัตราแลกเปลี่ยนเป็นเงินบาท แล้วจึงปัดเศษอีกครั้ง
เครื่องคิดเลขคำนวณได้ 4636298.55 แต่โปรแกรมคำนวณได้ 4636298.21 ไม่ว่าจะทดสอบกับคอมพิวเตอร์เครื่องไหน อย่างน้อยก็สบายใจว่าไม่ได้เกิดจากความผิดปกติของฮาร์ดแวร์หรือพลังเหนือธรรมชาติใดๆ แม้เราจะอุทานว่า “ผีหลอก”
(2)
เรื่องมาถึงโปรแกรมเมอร์ เช่นเดียวกัน มนุษย์ที่เขียนโค้ดได้ก็ไม่อาจแน่ใจตัวเอง บางทีอาจหละหลวมพลาดตรงจุดไหนก็ได้ จึงพยายามไล่ตรวจอย่างละเอียด ทั้งที่ยังจับต้นชนปลายไม่ถูก
วนเวียนหาทางทดสอบ ลองผิดลองถูกมาทีละ step จนสังเกตเห็นว่า มูลค่าปัดเศษก่อนคูณอัตราแลกเปลี่ยนคลาดเคลื่อน (ตามที่เล่ามาข้างต้น)
หากดูตามภาพตัวอย่างจะพบว่า ผลคูณแทนที่ทศนิยมตำแหน่งที่สามเป็น 5 กลับได้แค่ 4999..
ความพิสดารชั้นที่สอง Math.round ปัดเศษ .**4999.. ส่วนใหญ่ก็ปัดขึ้นได้ถูกต้อง มีเพียงบางค่าที่ปัดลง จนทำให้มองข้ามไปทีแรก
สิ่งที่เห็น อาจไม่ใช่สิ่งที่เป็นสำหรับ JavaScript
5.524999... ปัดเศษ "ขึ้น" ได้ 5.53 แต่ 8.524999... ทำไมปัดเศษ "ลง" ได้ 8.52
เท่านี้เอง.. ไม่รู้จะหัวเราะหรือร้องไห้ดี
เดินหาแว่นตาทั่วบ้าน แล้วพบว่ามันอยู่บนหน้าผาก

(3)
เพื่อเข้าใจขนาดของปัญหา ผมจึงลองเขียนโค้ดเพื่อตรวจสอบว่ามีเลขคู่ไหนบ้าง เมื่อคูณกันแล้วปัดเศษเพี้ยน เลขข้างหนึ่งทศนิยม 1 ตำแหน่ง อีกข้างหนึ่งทศนิยม 2 ตำแหน่ง
(num1*num2): 1.7 * 5.25 = 8.924999999999999
>> round 8.92 **** w/adj 8.925 >> 8.93
(num1*num2): 1.7 * 11.75 = 19.974999999999998
>> round 19.97 **** w/adj 19.975 >> 19.98
(num1*num2): 1.7 * 19.75 = 33.574999999999996
>> round 33.57 **** w/adj 33.575 >> 33.58
(num1*num2): 1.7 * 22.25 = 37.824999999999996
>> round 37.82 **** w/adj 37.825 >> 37.83
(num1*num2): 1.7 * 38.25 = 65.02499999999999
>> round 65.02 **** w/adj 65.025 >> 65.03
(num1*num2): 1.7 * 40.75 = 69.27499999999999
>> round 69.27 **** w/adj 69.275 >> 69.28
(num1*num2): 1.7 * 43.25 = 73.52499999999999
>> round 73.52 **** w/adj 73.525 >> 73.53
(num1*num2): 1.7 * 45.75 = 77.77499999999999
>> round 77.77 **** w/adj 77.775 >> 77.78
(num1*num2): 1.9 * 2.25 = 4.2749999999999995
>> round 4.27 **** w/adj 4.275 >> 4.28
(num1*num2): 1.9 * 7.25 = 13.774999999999999
>> round 13.77 **** w/adj 13.775 >> 13.78
(num1*num2): 1.9 * 10.25 = 19.474999999999998
>> round 19.47 **** w/adj 19.475 >> 19.48
(num1*num2): 1.9 * 13.25 = 25.174999999999997
>> round 25.17 **** w/adj 25.175 >> 25.18
(num1*num2): 1.9 * 15.75 = 29.924999999999997
>> round 29.92 **** w/adj 29.925 >> 29.93
(num1*num2): 1.9 * 19.25 = 36.574999999999996
>> round 36.57 **** w/adj 36.575 >> 36.58
(num1*num2): 1.9 * 25.25 = 47.974999999999994
>> round 47.97 **** w/adj 47.975 >> 47.98
(num1*num2): 1.9 * 27.75 = 52.724999999999994
>> round 52.72 **** w/adj 52.725 >> 52.73
(num1*num2): 1.9 * 30.25 = 57.474999999999994
>> round 57.47 **** w/adj 57.475 >> 57.48
(num1*num2): 1.9 * 32.75 = 62.224999999999994
>> round 62.22 **** w/adj 62.225 >> 62.23
(num1*num2): 1.9 * 34.75 = 66.02499999999999
>> round 66.02 **** w/adj 66.025 >> 66.03
(num1*num2): 1.9 * 37.25 = 70.77499999999999
>> round 70.77 **** w/adj 70.775 >> 70.78
(num1*num2): 1.9 * 39.75 = 75.52499999999999
>> round 75.52 **** w/adj 75.525 >> 75.53
(num1*num2): 1.9 * 42.25 = 80.27499999999999
>> round 80.27 **** w/adj 80.275 >> 80.28
(num1*num2): 1.9 * 49.25 = 93.57499999999999
>> round 93.57 **** w/adj 93.575 >> 93.58จะเห็นว่าหลายชุดเป็นเลขที่มีโอกาสพบเจอได้ นับว่ามีความเสี่ยงอยู่พอสมควรที่ทำให้ผู้ใช้รู้สึกว่าโปรแกรมของเราโง่ แค่คิดเลขง่ายๆ ยังผิด มีผลต่อความน่าเชื่อถือ โดยเฉพาะเมื่อใช้กับธุรกิจที่หน่วยซื้อขายไม่เป็นจำนวนเต็ม แถมราคาต่อหน่วยมีเศษสตางค์ด้วย โชคดีที่ธุรกิจส่วนใหญ่ขายของเป็นหน่วยจำนวนเต็ม ชิ้น, กล่อง, อัน ฯลฯ ไม่ค่อยมีโอกาสเจอเคสอย่างนี้
สำหรับผม JavaScript ยังเป็นภาษาที่สนุกท้าทาย แต่ความยืดหยุ่นสูงทำให้เกิดความเหลื่อมล้ำระหว่างคุณภาพของโค้ดที่ดีกับไม่ดี จึงไม่เหมาะกับใช้ในทีมใหญ่เพราะควบคุมคุณภาพยาก เช่นเดียวกับความสัมพันธ์อื่นในชีวิตเรา กับคนบางคน กับสิ่งบางสิ่ง มีทั้งส่วนที่ควบคุมได้และควบคุมไม่ได้ ไม่มีใครหรือสิ่งไหนสมบูรณ์แบบ เมื่อเข้าใจข้อบกพร่อง ยอมรับจุดอ่อนและจุดแข็งก็ช่วยให้รู้ว่าควรเลือกวิธีอยู่ร่วมกันได้อย่างไร
โค้ดทดสอบ



ความคิดเห็น