top of page
ค้นหา

JavaScript, Rounded Number Inaccurate Outcome

  • รูปภาพนักเขียน: Sathit Jittanupat
    Sathit Jittanupat
  • 1 ก.พ. 2568
  • ยาว 2 นาที

อัปเดตเมื่อ 3 ก.พ. 2568


สินค้าที่ตั้งราคาตามน้ำหนัก (กิโลกรัม หรือ ตัน) หรือความยาว​ (เมตร) มักมีปัญหาความคลาดเคลื่อนในการคำนวณมูลค่าสุทธิ เพราะหลายครั้งที่ตัวเลขหน่วยที่ชั่งน้ำหนักหรือวัดความยาวได้มักจะมีทศนิยม 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 ยังเป็นภาษาที่สนุกท้าทาย แต่ความยืดหยุ่นสูงทำให้เกิดความเหลื่อมล้ำระหว่างคุณภาพของโค้ดที่ดีกับไม่ดี จึงไม่เหมาะกับใช้ในทีมใหญ่เพราะควบคุมคุณภาพยาก เช่นเดียวกับความสัมพันธ์อื่นในชีวิตเรา กับคนบางคน กับสิ่งบางสิ่ง มีทั้งส่วนที่ควบคุมได้และควบคุมไม่ได้ ไม่มีใครหรือสิ่งไหนสมบูรณ์แบบ เมื่อเข้าใจข้อบกพร่อง ยอมรับจุดอ่อนและจุดแข็งก็ช่วยให้รู้ว่าควรเลือกวิธีอยู่ร่วมกันได้อย่างไร


โค้ดทดสอบ



 
 
 

ความคิดเห็น


Post: Blog2_Post
  • Facebook

©2020 by Scraft On Cloud. Proudly created with Wix.com

bottom of page