Câu hỏi này rất phức tạp.
Giả sử chúng ta có một hàm, roundTo2DP(num)
lấy một số float làm đối số và trả về giá trị được làm tròn đến 2 vị trí thập phân. Mỗi biểu thức này nên đánh giá cái gì?
roundTo2DP(0.014999999999999999)
roundTo2DP(0.0150000000000000001)
roundTo2DP(0.015)
Câu trả lời 'rõ ràng' là ví dụ đầu tiên nên làm tròn đến 0,01 (vì nó gần 0,01 hơn 0,02) trong khi hai ví dụ còn lại làm tròn đến 0,02 (vì 0,0150000000000000001 gần với 0,02 hơn 0,01 và vì 0,015 nằm chính xác giữa chúng và có một quy ước toán học rằng những con số như vậy được làm tròn lên).
Cái bẫy mà bạn có thể đoán được là roundTo2DP
không thể thực hiện được để đưa ra những câu trả lời rõ ràng đó, bởi vì cả ba số được truyền cho nó đều là cùng một số . Các số dấu phẩy động nhị phân của IEEE 754 (loại được sử dụng bởi JavaScript) không thể biểu thị chính xác hầu hết các số không nguyên và vì vậy cả ba chữ số ở trên được làm tròn thành một số dấu phẩy động hợp lệ gần đó. Con số này, như nó xảy ra, là chính xác
0,01199999999999999944488848768742172978818416595458984375
gần với 0,01 hơn 0,02.
Bạn có thể thấy rằng cả ba số đều giống nhau trên bảng điều khiển trình duyệt, trình bao Node hoặc trình thông dịch JavaScript khác. Chỉ cần so sánh chúng:
> 0.014999999999999999 === 0.0150000000000000001
true
Vì vậy, khi tôi viết m = 0.0150000000000000001
, giá trị chính xác củam
cái mà tôi kết thúc gần 0.01
với nó hơn 0.02
. Tuy nhiên, nếu tôi chuyển đổi m
thành Chuỗi ...
> var m = 0.0150000000000000001;
> console.log(String(m));
0.015
> var m = 0.014999999999999999;
> console.log(String(m));
0.015
... Tôi nhận được 0,015, làm tròn thành 0,02 và đáng chú ý không phải là số thập phân 56 mà tôi đã nói trước đó nói rằng tất cả các số này đều chính xác bằng. Vậy ma thuật đen tối này là gì?
Câu trả lời có thể được tìm thấy trong đặc tả ECMAScript, trong phần 7.1.12.1: ToString được áp dụng cho loại Số . Ở đây các quy tắc để chuyển đổi một số Số m thành Chuỗi được đặt ra. Phần quan trọng là điểm 5, trong đó một số nguyên s được tạo ra có chữ số sẽ được sử dụng trong chuỗi đại diện của m :
Đặt n , k và s là các số nguyên sao cho k ≥ 1, 10 k -1 ≤ s <10 k , giá trị Số cho s × 10 n - k là m và k càng nhỏ càng tốt. Lưu ý rằng k là số chữ số trong biểu diễn thập phân của s , s đó không chia hết cho 10 và chữ số có nghĩa nhỏ nhất của s không nhất thiết phải được xác định duy nhất bởi các tiêu chí này.
Phần quan trọng ở đây là yêu cầu " k càng nhỏ càng tốt". Yêu cầu đó là bao nhiêu là một yêu cầu, với một Số m
, giá trị của String(m)
phải có số chữ số ít nhất có thể trong khi vẫn đáp ứng yêu cầu đó Number(String(m)) === m
. Vì chúng ta đã biết rằng0.015 === 0.0150000000000000001
, giờ thì rõ ràng tại sao String(0.0150000000000000001) === '0.015'
phải đúng.
Tất nhiên, không có cuộc thảo luận nào đã trả lời trực tiếp những gì roundTo2DP(m)
nên trả lại. Nếu m
giá trị chính xác của là 0,01199999999999999944488848768742172978818416595458984375, nhưng đại diện Chuỗi của nó là '0,015', thì đó là gì câu trả lời đúng - về mặt toán học, thực tế, triết học hay bất cứ điều gì - khi chúng ta làm tròn nó thành hai số thập phân?
Không có câu trả lời đúng duy nhất cho điều này. Nó phụ thuộc vào trường hợp sử dụng của bạn. Bạn có thể muốn tôn trọng biểu diễn Chuỗi và làm tròn lên khi:
- Giá trị được đại diện là rời rạc, ví dụ như một lượng tiền tệ trong một loại tiền có 3 chữ số thập phân như dinar. Trong trường hợp này, giá trị thực của một Số như 0,015 là 0,015 và đại diện 0,0119999999 ... mà nó nhận được ở dấu phẩy động nhị phân là lỗi làm tròn. (Tất nhiên, nhiều người sẽ tranh luận, một cách hợp lý, rằng bạn nên sử dụng thư viện thập phân để xử lý các giá trị đó và không bao giờ biểu thị chúng dưới dạng Số dấu phẩy động nhị phân ở vị trí đầu tiên.)
- Giá trị được gõ bởi một người dùng. Trong trường hợp này, một lần nữa, số thập phân chính xác được nhập là "đúng" hơn so với biểu diễn dấu phẩy động nhị phân gần nhất.
Mặt khác, bạn có thể muốn tôn trọng giá trị dấu phẩy động nhị phân và làm tròn xuống khi giá trị của bạn từ thang đo liên tục vốn có - ví dụ, nếu đó là cách đọc từ cảm biến.
Hai cách tiếp cận này yêu cầu mã khác nhau. Để tôn trọng biểu diễn Chuỗi của Số, chúng tôi có thể (với khá nhiều mã tinh tế hợp lý) thực hiện làm tròn số của chúng tôi hoạt động trực tiếp trên biểu diễn Chuỗi, chữ số theo chữ số, sử dụng cùng thuật toán bạn đã sử dụng ở trường khi bạn được dạy cách làm tròn số. Dưới đây là một ví dụ tôn trọng yêu cầu của OP về việc biểu thị số đến 2 vị trí thập phân "chỉ khi cần thiết" bằng cách tước các số 0 ở sau dấu thập phân; tất nhiên, bạn có thể cần phải điều chỉnh nó theo nhu cầu chính xác của mình.
/**
* 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+$/, '')
}
Ví dụ sử dụng:
> 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'
Chức năng trên có lẽ là những gì bạn muốn sử dụng để tránh người dùng chứng kiến những con số mà họ đã nhập bị làm tròn sai.
(Để thay thế, bạn cũng có thể thử thư viện round10 cung cấp chức năng tương tự với cách triển khai cực kỳ khác biệt.)
Nhưng điều gì sẽ xảy ra nếu bạn có loại Số thứ hai - một giá trị được lấy từ thang đo liên tục, trong đó không có lý do gì để nghĩ rằng các biểu diễn thập phân gần đúng với số thập phân ít chính xác hơn các số có nhiều số thập phân hơn? Trong trường hợp đó, chúng tôi không muốn tôn trọng biểu diễn Chuỗi, vì biểu diễn đó (như được giải thích trong thông số kỹ thuật) đã được sắp xếp theo thứ tự; chúng tôi không muốn phạm sai lầm khi nói "0,011999999 ... 375 vòng lên tới 0,015, làm tròn đến 0,02, vì vậy 0,011999999 ... 375 vòng lên 0,02".
Ở đây chúng ta chỉ cần sử dụng toFixed
phương thức tích hợp sẵn. Lưu ý rằng bằng cách gọi Number()
Chuỗi được trả về bởi toFixed
, chúng ta sẽ nhận được một Số có biểu diễn Chuỗi không có các số 0 ở cuối (nhờ cách JavaScript tính toán biểu diễn Chuỗi của một Số, đã thảo luận trước đó trong câu trả lời này).
/**
* 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);
}