คำถามนี้ซับซ้อน
สมมติว่าเรามีฟังก์ชั่นซึ่งroundTo2DP(num)
รับค่าทศนิยมเป็นอาร์กิวเมนต์และส่งคืนค่าที่ปัดเศษเป็นทศนิยม 2 ตำแหน่ง นิพจน์เหล่านี้แต่ละสิ่งควรประเมินเป็นอย่างไร
roundTo2DP(0.014999999999999999)
roundTo2DP(0.0150000000000000001)
roundTo2DP(0.015)
คำตอบ 'ชัดเจน' คือตัวอย่างแรกควรปัดเศษเป็น 0.01 (เพราะใกล้ถึง 0.01 มากกว่า 0.02) ในขณะที่อีกสองรอบเป็น 0.02 (เพราะ 0.01500000000000000000001 ใกล้กับ 0.02 มากกว่า 0.01 และ 0.015 อยู่ครึ่งทาง พวกเขาและมีการประชุมทางคณิตศาสตร์ที่ตัวเลขดังกล่าวได้ปัดเศษขึ้น)
จับซึ่งคุณอาจจะเดาได้ว่าroundTo2DP
ไม่อาจดำเนินการเพื่อให้คำตอบที่ชัดเจนเหล่านั้นเพราะทุกหมายเลขสามผ่านไปเป็นหมายเลขเดียวกัน หมายเลขจุดลอยตัวเลขฐานสองของ IEEE 754 (ชนิดที่ใช้โดย JavaScript) ไม่สามารถแสดงตัวเลขที่ไม่ใช่จำนวนเต็มได้อย่างแท้จริงดังนั้นตัวเลขทั้งสามตัวด้านบนจึงถูกปัดเศษเป็นจำนวนจุดลอยตัวที่ถูกต้อง หมายเลขนี้ที่มันเกิดขึ้นคือว่า
0,01499999999999999944488848768742172978818416595458984375
ซึ่งใกล้เคียงกับ 0.01 มากกว่า 0.02
คุณจะเห็นว่าตัวเลขทั้งสามนั้นเหมือนกันที่คอนโซลเบราว์เซอร์ของคุณโหนดของ Node หรือล่าม JavaScript อื่น ๆ เพียงเปรียบเทียบพวกเขา:
> 0.014999999999999999 === 0.0150000000000000001
true
ดังนั้นเมื่อผมเขียนm = 0.0150000000000000001
ที่คุ้มค่าแน่นอนของm
ที่ฉันจบลงด้วยการอยู่ใกล้กว่าก็คือการ0.01
0.02
และถ้าฉันแปลงm
เป็นสตริง ...
> var m = 0.0150000000000000001;
> console.log(String(m));
0.015
> var m = 0.014999999999999999;
> console.log(String(m));
0.015
... ฉันได้ 0.015 ซึ่งควรจะเป็น 0.02 และไม่เด่นชัดว่าเลขทศนิยม 56 ตำแหน่งที่ฉันกล่าวไว้ก่อนหน้านี้ว่าตัวเลขเหล่านี้ทั้งหมดเท่ากับ นี่คือเวทย์มนตร์มืดอะไร?
คำตอบที่สามารถพบได้ในข้อกำหนด ECMAScript ในส่วน7.1.12.1: ToString นำไปใช้กับประเภทจำนวน นี่คือกฎสำหรับการแปลง Number mบางส่วนเป็น String ส่วนสำคัญคือจุด 5 ซึ่งสร้างจำนวนเต็มsซึ่งจะใช้ตัวเลขในการแทนค่าสตริงของm :
ให้n , k , และsเป็นจำนวนเต็มเช่นที่k ≥ 1, 10 k -1 ≤ s <10 k , ค่า Number สำหรับs × 10 n - kคือm , และkมีค่าน้อยที่สุด โปรดทราบว่า k คือจำนวนตัวเลขในการแทนค่าทศนิยมของs , sนั้นหารด้วย 10 ไม่ได้และหลักสำคัญน้อยที่สุดของsไม่จำเป็นต้องพิจารณาโดยไม่ซ้ำกันตามเกณฑ์เหล่านี้
ส่วนสำคัญที่นี่คือข้อกำหนดที่ว่า " kมีขนาดเล็กที่สุด" สิ่งที่ต้องการที่จำนวนเงินที่เป็นความต้องการที่กำหนดจำนวนm
ค่าของString(m)
ต้องมีจำนวนน้อยที่สุดของตัวเลขNumber(String(m)) === m
ขณะที่ยังคงความพึงพอใจความต้องการว่า เนื่องจากเรารู้แล้ว0.015 === 0.0150000000000000001
ตอนนี้มันชัดเจนว่าทำไมString(0.0150000000000000001) === '0.015'
ต้องเป็นจริง
แน่นอนว่าการสนทนานี้ไม่ได้ตอบสิ่งที่roundTo2DP(m)
ควรกลับโดยตรง ถ้าm
ค่าที่แน่นอนคือ 0.01499999999999999944488848768742172978818416595458984375 แต่การแทนค่าสตริงของมันคือ '0.015' แล้วคำตอบที่ถูกต้องคืออะไร- ทางคณิตศาสตร์, ทางปฏิบัติ, ปรัชญาหรืออะไรก็ตาม - เมื่อเราปัดเศษเป็นทศนิยมสองตำแหน่ง?
ไม่มีคำตอบที่ถูกต้องสำหรับสิ่งนี้ มันขึ้นอยู่กับกรณีการใช้งานของคุณ คุณอาจต้องการเคารพการเป็นตัวแทนสตริงและปัดเศษขึ้นเมื่อ:
- ค่าที่จะแสดงนั้นไม่ต่อเนื่องโดยเนื้อแท้เช่นจำนวนเงินในสกุลเงิน 3 ตำแหน่งทศนิยมเช่นดินาร์ ในกรณีนี้ค่าจริงของตัวเลขเช่น 0.015 คือ 0.015 และ 0.0149999999 ... การแทนค่าที่ได้รับในเลขทศนิยมแบบไบนารีนั้นเป็นข้อผิดพลาดในการปัดเศษ (แน่นอนว่าหลายคนจะโต้แย้งเหตุผลว่าคุณควรใช้ห้องสมุดทศนิยมสำหรับการจัดการค่าดังกล่าวและไม่เคยแสดงให้พวกเขาเป็นตัวเลขทศนิยมเลขฐานสองในสถานที่แรก)
- ค่าถูกพิมพ์โดยผู้ใช้ ในกรณีนี้อีกครั้งจำนวนทศนิยมที่ป้อนจริงคือ 'จริง' มากกว่าการเป็นตัวแทนทศนิยมเลขฐานสองที่ใกล้ที่สุด
ในทางกลับกันคุณอาจต้องการเคารพค่าเลขฐานสองแบบลอยตัวและปัดเศษลงเมื่อค่าของคุณมาจากสเกลต่อเนื่องโดยเนื้อแท้ - ตัวอย่างเช่นหากเป็นการอ่านจากเซ็นเซอร์
วิธีการทั้งสองนี้ต้องใช้รหัสที่แตกต่างกัน เพื่อเคารพการแสดงสตริงของ Number เราสามารถ (ด้วยโค้ดที่ละเอียดอ่อนพอสมควร) ใช้การปัดเศษของเราเองซึ่งทำหน้าที่โดยตรงกับการแทนสตริงตัวเลขโดยตัวเลขโดยใช้อัลกอริทึมแบบเดียวกับที่คุณเคยใช้ในโรงเรียนเมื่อคุณ ได้รับการสอนวิธีการปัดเศษตัวเลข ด้านล่างเป็นตัวอย่างที่แสดงถึงความต้องการของ OP ในการแสดงจำนวนทศนิยม 2 ตำแหน่ง "เมื่อจำเป็น" โดยการดึงเลขศูนย์ต่อท้ายหลังจุดทศนิยม แน่นอนคุณอาจต้องปรับแต่งตามความต้องการที่แม่นยำของคุณ
/**
* Converts num to a decimal string (if it isn't one already) and then rounds it
* to at most dp decimal places.
*
* For explanation of why you'd want to perform rounding operations on a String
* rather than a Number, see http://stackoverflow.com/a/38676273/1709587
*
* @param {(number|string)} num
* @param {number} dp
* @return {string}
*/
function roundStringNumberWithoutTrailingZeroes (num, dp) {
if (arguments.length != 2) throw new Error("2 arguments required");
num = String(num);
if (num.indexOf('e+') != -1) {
// Can't round numbers this large because their string representation
// contains an exponent, like 9.99e+37
throw new Error("num too large");
}
if (num.indexOf('.') == -1) {
// Nothing to do
return num;
}
var parts = num.split('.'),
beforePoint = parts[0],
afterPoint = parts[1],
shouldRoundUp = afterPoint[dp] >= 5,
finalNumber;
afterPoint = afterPoint.slice(0, dp);
if (!shouldRoundUp) {
finalNumber = beforePoint + '.' + afterPoint;
} else if (/^9+$/.test(afterPoint)) {
// If we need to round up a number like 1.9999, increment the integer
// before the decimal point and discard the fractional part.
finalNumber = Number(beforePoint)+1;
} else {
// Starting from the last digit, increment digits until we find one
// that is not 9, then stop
var i = dp-1;
while (true) {
if (afterPoint[i] == '9') {
afterPoint = afterPoint.substr(0, i) +
'0' +
afterPoint.substr(i+1);
i--;
} else {
afterPoint = afterPoint.substr(0, i) +
(Number(afterPoint[i]) + 1) +
afterPoint.substr(i+1);
break;
}
}
finalNumber = beforePoint + '.' + afterPoint;
}
// Remove trailing zeroes from fractional part before returning
return finalNumber.replace(/0+$/, '')
}
ตัวอย่างการใช้งาน:
> roundStringNumberWithoutTrailingZeroes(1.6, 2)
'1.6'
> roundStringNumberWithoutTrailingZeroes(10000, 2)
'10000'
> roundStringNumberWithoutTrailingZeroes(0.015, 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes('0.015000', 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes(1, 1)
'1'
> roundStringNumberWithoutTrailingZeroes('0.015', 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes(0.01499999999999999944488848768742172978818416595458984375, 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes('0.01499999999999999944488848768742172978818416595458984375', 2)
'0.01'
ฟังก์ชั่นด้านบนอาจเป็นสิ่งที่คุณต้องการใช้เพื่อหลีกเลี่ยงผู้ใช้ที่เคยเห็นตัวเลขที่พวกเขาป้อนถูกปัดเศษผิด
(เป็นอีกทางเลือกหนึ่งคุณสามารถลองใช้ไลบรารีround10ซึ่งมีฟังก์ชั่นการทำงานที่คล้ายคลึงกันด้วยการปรับใช้ที่แตกต่างกันอย่างดุเดือด)
แต่ถ้าคุณมี Number แบบที่สอง - ค่าที่นำมาจากสเกลต่อเนื่องซึ่งไม่มีเหตุผลที่จะคิดว่าการแทนค่าทศนิยมโดยประมาณที่มีทศนิยมน้อยกว่านั้นจะแม่นยำกว่าที่มีมากขึ้นหรือไม่ ในกรณีดังกล่าวเราไม่ต้องการเคารพการแทนค่าสตริงเนื่องจากการแสดงดังกล่าว (ตามที่อธิบายไว้ในสเป็ค) นั้นเรียงลำดับแล้ว เราไม่ต้องการทำผิดพลาดในการพูดว่า "0.014999999 ... 375 รอบสูงสุด 0.015 ซึ่งรอบมากถึง 0.02 ดังนั้น 0.014999999 ... 375 รอบสูงสุด 0.02"
ที่นี่เราสามารถใช้toFixed
วิธีการในตัว โปรดทราบว่าด้วยการเรียกNumber()
ใช้ String ที่ส่งคืนโดยtoFixed
เราจะได้รับหมายเลขที่การแทนค่าสตริงไม่มีศูนย์ต่อท้าย (ด้วยวิธีที่ JavaScript คำนวณการแทนค่าสตริงของตัวเลขที่กล่าวถึงก่อนหน้านี้ในคำตอบนี้)
/**
* Takes a float and rounds it to at most dp decimal places. For example
*
* roundFloatNumberWithoutTrailingZeroes(1.2345, 3)
*
* returns 1.234
*
* Note that since this treats the value passed to it as a floating point
* number, it will have counterintuitive results in some cases. For instance,
*
* roundFloatNumberWithoutTrailingZeroes(0.015, 2)
*
* gives 0.01 where 0.02 might be expected. For an explanation of why, see
* http://stackoverflow.com/a/38676273/1709587. You may want to consider using the
* roundStringNumberWithoutTrailingZeroes function there instead.
*
* @param {number} num
* @param {number} dp
* @return {number}
*/
function roundFloatNumberWithoutTrailingZeroes (num, dp) {
var numToFixedDp = Number(num).toFixed(dp);
return Number(numToFixedDp);
}