Vấn đề với các giá trị dấu phẩy động là chúng đang cố gắng biểu diễn một lượng vô hạn các giá trị (liên tục) với một lượng bit cố định. Vì vậy, một cách tự nhiên, phải có một số mất mát khi chơi và bạn sẽ bị cắn với một số giá trị.
Khi một máy tính lưu trữ 1.275 dưới dạng giá trị dấu phẩy động, thực tế nó sẽ không nhớ đó là 1.275 hay 1.27499999999999993 hay thậm chí 1.27500000000000002. Các giá trị này sẽ cho kết quả khác nhau sau khi làm tròn đến hai số thập phân, nhưng chúng sẽ không, vì đối với máy tính, chúng trông giống hệt nhau sau khi lưu trữ dưới dạng giá trị dấu phẩy động và không có cách nào để khôi phục dữ liệu bị mất. Bất kỳ tính toán thêm sẽ chỉ tích lũy sự thiếu chính xác như vậy.
Vì vậy, nếu độ chính xác quan trọng, bạn phải tránh các giá trị dấu phẩy động ngay từ đầu. Các tùy chọn đơn giản nhất là
- sử dụng một thư viện dành riêng
- sử dụng các chuỗi để lưu trữ và chuyển xung quanh các giá trị (kèm theo các thao tác chuỗi)
- sử dụng số nguyên (ví dụ: bạn có thể chuyển khoảng số phần trăm giá trị thực của mình, ví dụ: số tiền tính bằng xu thay vì số tiền tính bằng đô la)
Ví dụ: khi sử dụng số nguyên để lưu trữ số phần trăm, hàm tìm giá trị thực khá đơn giản:
function descale(num, decimals) {
var hasMinus = num < 0;
var numString = Math.abs(num).toString();
var precedingZeroes = '';
for (var i = numString.length; i <= decimals; i++) {
precedingZeroes += '0';
}
numString = precedingZeroes + numString;
return (hasMinus ? '-' : '')
+ numString.substr(0, numString.length-decimals)
+ '.'
+ numString.substr(numString.length-decimals);
}
alert(descale(127, 2));
Với các chuỗi, bạn sẽ cần làm tròn, nhưng vẫn có thể quản lý được:
function precise_round(num, decimals) {
var parts = num.split('.');
var hasMinus = parts.length > 0 && parts[0].length > 0 && parts[0].charAt(0) == '-';
var integralPart = parts.length == 0 ? '0' : (hasMinus ? parts[0].substr(1) : parts[0]);
var decimalPart = parts.length > 1 ? parts[1] : '';
if (decimalPart.length > decimals) {
var roundOffNumber = decimalPart.charAt(decimals);
decimalPart = decimalPart.substr(0, decimals);
if ('56789'.indexOf(roundOffNumber) > -1) {
var numbers = integralPart + decimalPart;
var i = numbers.length;
var trailingZeroes = '';
var justOneAndTrailingZeroes = true;
do {
i--;
var roundedNumber = '1234567890'.charAt(parseInt(numbers.charAt(i)));
if (roundedNumber === '0') {
trailingZeroes += '0';
} else {
numbers = numbers.substr(0, i) + roundedNumber + trailingZeroes;
justOneAndTrailingZeroes = false;
break;
}
} while (i > 0);
if (justOneAndTrailingZeroes) {
numbers = '1' + trailingZeroes;
}
integralPart = numbers.substr(0, numbers.length - decimals);
decimalPart = numbers.substr(numbers.length - decimals);
}
} else {
for (var i = decimalPart.length; i < decimals; i++) {
decimalPart += '0';
}
}
return (hasMinus ? '-' : '') + integralPart + (decimals > 0 ? '.' + decimalPart : '');
}
alert(precise_round('1.275', 2));
alert(precise_round('1.27499999999999993', 2));
Lưu ý rằng chức năng này làm tròn đến gần nhất, liên kết từ 0 , trong khi IEEE 754 khuyến nghị làm tròn đến gần nhất, liên kết ngay cả với tư cách là hành vi mặc định cho các hoạt động của dấu phẩy động. Những sửa đổi như vậy được để lại như một bài tập cho người đọc :)