You've said you want 5.425
to round to 5.42
but that you want 5.555556
to round to 5.556
. That means you're doing a round-ties-down (or perhaps round-ties-toward-zero) operation rather than a "normal" round where ties go up (or away from zero, depending).
The only way I can think of to do that is to subtract one from the final digit of the number at each stage of rounding, like this:
// 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;
}
Live Example:
// 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>
Note that last result for the negative number. If you want to round toward zero rather than "down," you change this line:
n = Math.round((n - subtrahend) * multiplier) / multiplier;
to this, which allows for the sign of n
:
n = Math.round((n + (n < 0 ? subtrahend : -subtrahend)) * multiplier) / multiplier;
and change the name of the function, since it no longer rounds down (perhaps roundTiesToZero
).
Live Example:
// 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>