2 回答

TA貢獻1817條經驗 獲得超14個贊
問題是,JavaScript 不知道5.425. 計算機的浮點實現以及大多數編程語言的浮點實現都是基于二進制的,而不是基于十進制的(參見 IEEE 754)。因此,每個掃描的數字首先四舍五入到最接近的可能值:
5.425 .toPrecision(18); // result "5.42499999999999982" => ToFixed(2)="5.42"
2.336 .toPrecision(18); // result "2.33599999999999985" => ToFixed(2)="2.34"
0.5556 .toPrecision(18); // result "0.555599999999999983" => ToFixed(2)="0.56"
并將這些掃描結果用于進一步處理。所以使用的結果toFixed(2)是預期的并且絕對正確。
并且不要相信其他解決方案(有時稱為betterToFixed())來解決此問題。他們對其他數字有疑問,因為許多作者不了解幕后的數學。

TA貢獻1895條經驗 獲得超7個贊
你說你想5.425四舍五入,5.42但你想5.555556四舍五入5.556。這意味著您正在執行向下舍入(或可能是向零舍入)操作,而不是在“正常”回合中增加(或遠離零,取決于)。
我能想到的唯一方法是在四舍五入的每個階段從數字的最后一位中減去一個,如下所示:
// Round the given number to the given number of places, but rounding ties down
// (0.5 rounds to 0 instead of 1, 0.55 rounds to 0.5 instead of 0.6, etc.)
function roundTiesDown(n, places) {
if (places < 1) {
throw new Error(`Received places=${places}, but must be >=1`);
}
let currentPlaces = significantFractionalPlaces(n)
// Round ties down at each level, e.g. (if places = 2):
// 0.55556 => 0.5556
// 0.5556 => 0.556
// 0.556 => 0.56
// and
// 0.55555 => 0.5555 (would be 0.5556 with "normal" rounding)
// 0.5555 => 0.555
// 0.555 => 0.55
while (currentPlaces > places) {
const subtrahend = 1 / Math.pow(10, currentPlaces);
--currentPlaces;
const multiplier = Math.pow(10, currentPlaces);
n = Math.round((n - subtrahend) * multiplier) / multiplier;
}
return n;
}
現場示例:
// Get a function to convert numbers to string without going to scientific notation
let numberToString;
if (typeof Intl === "object" && typeof Intl.NumberFormat === "function") {
// Intl.NumberFormat lets us do this properly
const format = new Intl.NumberFormat(undefined, {
style: "decimal",
useGrouping: false,
maximumFractionDigits: 20
});
numberToString = n => format.format(n);
} else {
// Fall back to toString on platforms without Intl.NumberFormat
// (all major browsers have it, including IE11 -
// https://caniuse.com/#feat=mdn-javascript_builtins_intl_numberformat)
const rexContainsE = /e/i;
numberToString = n => {
const str = n.toString();
if (rexContainsE.test(str)) {
// Went to scientific notation
throw new Error("Can't handle numbers this big on this platform");
}
return str;
};
}
// Get the currentPlaces number of significant places in the given number
function significantFractionalPlaces(n) {
const str = numberToString(n);
const idx = str.indexOf(".");
return idx === -1 ? 0 : str.length - idx - 1;
}
// Round the given number to the given number of places, but rounding ties down
// (0.5 rounds to 0 instead of 1, 0.55 rounds to 0.5 instead of 0.6, etc.)
function roundTiesDown(n, places) {
if (places < 1) {
throw new Error(`Received places=${places}, but must be >=1`);
}
let currentPlaces = significantFractionalPlaces(n)
// Round ties down at each level, e.g. (if places = 2):
// 0.55556 => 0.5556
// 0.5556 => 0.556
// 0.556 => 0.56
// and
// 0.55555 => 0.5555 (would be 0.5556 with "normal" rounding)
// 0.5555 => 0.555
// 0.555 => 0.55
while (currentPlaces > places) {
const subtrahend = 1 / Math.pow(10, currentPlaces);
--currentPlaces;
const multiplier = Math.pow(10, currentPlaces);
/* For your real function, use this:
n = Math.round((n - subtrahend) * multiplier) / multiplier;
instead of the following lines using `rounded`
*/
const rounded = Math.round((n - subtrahend) * multiplier) / multiplier;
if (verbose) {
log("detail", `Rounded ${n} to ${rounded}`);
}
n = rounded;
}
return n;
}
// ===== Testing
const cbVerbose = document.querySelector("input[type=checkbox]");
const btnRun = document.querySelector("input[type=button]");
const output = document.querySelector(".output");
const errors = document.querySelector(".errors");
function log(cls, msg) {
/*
output.insertAdjacentText("beforeend", "\r\n" + msgs.join(" "));
*/
const div = document.createElement("div");
div.className = cls;
div.textContent = msg;
output.appendChild(div);
}
let verbose = cbVerbose.checked;
function test(n, expected) {
const rounded = roundTiesDown(n, 2);
const good = rounded === expected;
log(
good ? "good" : "error",
`${n} => ${rounded} ${good ? "OK" : `<== ERROR, expected ${expected}`}`
);
return good ? 0 : 1;
}
function runTests() {
verbose = cbVerbose.checked;
output.textContent = "";
const errorcount =
test(5.425, 5.42) +
test(5.555556, 5.56) +
test(12.3456789, 12.35) +
test(1.125, 1.12) +
test(2.336, 2.34) +
test(2, 2) +
test(-5.425, -5.43);
errors.textContent = errorcount === 0 ? "All passed" : `Errors: ${errorcount}`;
errors.className = errorcount === 0 ? "good" : "error";
}
btnRun.addEventListener("click", runTests);
runTests();
html {
box-sizing: border-box;
font-family: sans-serif;
}
*, *:before, *:after {
box-sizing: inherit;
}
html, body {
height: 100%;
overflow: hidden;
padding: 0;
margin: 0;
}
body {
padding: 4px;
display: flex;
flex-direction: column;
font-size: 14px;
}
.panel {
order: 1;
border-bottom: 1px solid black;
padding-bottom: 2px;
}
.output {
order: 2;
flex-grow: 1;
white-space: pre;
font-family: monospace;
overflow: auto;
}
.good {
color: #060;
}
.error {
color: #C00;
}
.detail {
color: #aaa;
}
<div class="panel">
<label style="user-select: none">
<input type="checkbox">
Verbose output
</label>
<input type="button" value="Run Tests">
<span class="errors"></span>
</div>
<div class="output"></div>
請注意,最后一個結果為負數。如果您想向零舍入而不是“向下”,請更改此行:
n = Math.round((n - subtrahend) * multiplier) / multiplier;
對此,它允許 的符號n:
n = Math.round((n + (n < 0 ? subtrahend : -subtrahend)) * multiplier) / multiplier;
并更改函數的名稱,因為它不再向下舍入(也許roundTiesToZero)。
現場示例:
// Get a function to convert numbers to string without going to scientific notation
let numberToString;
if (typeof Intl === "object" && typeof Intl.NumberFormat === "function") {
// Intl.NumberFormat lets us do this properly
const format = new Intl.NumberFormat(undefined, {
style: "decimal",
useGrouping: false,
maximumFractionDigits: 20
});
numberToString = n => format.format(n);
} else {
// Fall back to toString on platforms without Intl.NumberFormat
// (all major browsers have it, including IE11 -
// https://caniuse.com/#feat=mdn-javascript_builtins_intl_numberformat)
const rexContainsE = /e/i;
numberToString = n => {
const str = n.toString();
if (rexContainsE.test(str)) {
// Went to scientific notation
throw new Error("Can't handle numbers this big on this platform");
}
return str;
};
}
// Get the currentPlaces number of significant places in the given number
function significantFractionalPlaces(n) {
const str = numberToString(n);
const idx = str.indexOf(".");
return idx === -1 ? 0 : str.length - idx - 1;
}
// Round the given number to the given number of places, but rounding ties down
// (0.5 rounds to 0 instead of 1, 0.55 rounds to 0.5 instead of 0.6, etc.)
function roundTiesToZero(n, places) {
if (places < 1) {
throw new Error(`Received places=${places}, but must be >=1`);
}
let currentPlaces = significantFractionalPlaces(n)
// Round ties down at each level, e.g. (if places = 2):
// 0.55556 => 0.5556
// 0.5556 => 0.556
// 0.556 => 0.56
// and
// 0.55555 => 0.5555 (would be 0.5556 with "normal" rounding)
// 0.5555 => 0.555
// 0.555 => 0.55
while (currentPlaces > places) {
const subtrahend = 1 / Math.pow(10, currentPlaces);
--currentPlaces;
const multiplier = Math.pow(10, currentPlaces);
/* For your real function, use this:
n = Math.round((n + (n < 0 ? subtrahend : -subtrahend)) * multiplier) / multiplier;
instead of the following lines using `rounded`
*/
const rounded = Math.round((n + (n < 0 ? subtrahend : -subtrahend)) * multiplier) / multiplier;
if (verbose) {
log("detail", `Rounded ${n} to ${rounded}`);
}
n = rounded;
}
return n;
}
// ===== Testing
const cbVerbose = document.querySelector("input[type=checkbox]");
const btnRun = document.querySelector("input[type=button]");
const output = document.querySelector(".output");
const errors = document.querySelector(".errors");
function log(cls, msg) {
/*
output.insertAdjacentText("beforeend", "\r\n" + msgs.join(" "));
*/
const div = document.createElement("div");
div.className = cls;
div.textContent = msg;
output.appendChild(div);
}
let verbose = cbVerbose.checked;
function test(n, expected) {
const rounded = roundTiesToZero(n, 2);
const good = rounded === expected;
log(
good ? "good" : "error",
`${n} => ${rounded} ${good ? "OK" : `<== ERROR, expected ${expected}`}`
);
return good ? 0 : 1;
}
function runTests() {
verbose = cbVerbose.checked;
output.textContent = "";
const errorcount =
test(5.425, 5.42) +
test(5.555556, 5.56) +
test(12.3456789, 12.35) +
test(1.125, 1.12) +
test(2.336, 2.34) +
test(2, 2) +
test(-5.425, -5.42);
errors.textContent = errorcount === 0 ? "All passed" : `Errors: ${errorcount}`;
errors.className = errorcount === 0 ? "good" : "error";
}
btnRun.addEventListener("click", runTests);
runTests();
html {
box-sizing: border-box;
font-family: sans-serif;
}
*, *:before, *:after {
box-sizing: inherit;
}
html, body {
height: 100%;
overflow: hidden;
padding: 0;
margin: 0;
}
body {
padding: 4px;
display: flex;
flex-direction: column;
font-size: 14px;
}
.panel {
order: 1;
border-bottom: 1px solid black;
padding-bottom: 2px;
}
.output {
order: 2;
flex-grow: 1;
white-space: pre;
font-family: monospace;
overflow: auto;
}
.good {
color: #080;
}
.error {
color: #C00;
}
.detail {
color: #aaa;
}
<div class="panel">
<label style="user-select: none">
<input type="checkbox">
Verbose output
</label>
<input type="button" value="Run Tests">
<span class="errors"></span>
</div>
<div class="output"></div>
添加回答
舉報