top of page
ค้นหา

JavaScript, Rounded Number Inaccurate Outcome

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

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

ree

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


ree

(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

เท่านี้เอง.. ไม่รู้จะหัวเราะหรือร้องไห้ดี


เดินหาแว่นตาทั่วบ้าน แล้วพบว่ามันอยู่บนหน้าผาก


ree

(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