roundToFractionDigits function
Rounds a number to a number of decimals, half away from zero, with plain arithmetic: the same result as Number(formatDecimal(value, maxFractionDigits)), much faster. Never returns -0.
ts
import { roundToFractionDigits } from 'utils';Signature
ts
export function roundToFractionDigits(value: number, maxFractionDigits = 3): numberParameters
| Name | Description |
|---|---|
value | The number to round; NaN and infinities come back unchanged. |
maxFractionDigits | The number of decimals to keep. Defaults to 3. |
Returns
The rounded number.
Example
ts
roundToFractionDigits(1.005, 2); // 1.01 (`toFixed` gives 1.00)
roundToFractionDigits(-2.5, 0); // -3Tests
Line coverage: 100 %.
- ✅ rounds 1.005 to 2 digits as 1.01
- ✅ rounds 1.004 to 2 digits as 1
- ✅ rounds -1.005 to 2 digits as -1.01
- ✅ rounds 2.5 to 0 digits as 3
- ✅ rounds -2.5 to 0 digits as -3
- ✅ rounds 0.285 to 2 digits as 0.29
- ✅ rounds 1.45 to 1 digits as 1.5
- ✅ rounds 123.456 to 1 digits as 123.5
- ✅ rounds 1e+21 to 2 digits as 1e+21
- ✅ rounds 1e-7 to 8 digits as 1e-7
- ✅ rounds 1.5 to 150 digits as 1.5
- ✅ never returns -0
- ✅ returns non-finite values unchanged
- ✅ matches formatDecimal on decimal inputs
- ✅ takes the defaults
Source
src/math/round-to-fraction-digits.ts (on GitHub)
Source code
ts
const DECIMAL_BASE = 10;
const TABLE_SIZE = 101;
const POWERS_OF_TEN: readonly number[] = /* @__PURE__ */ Array.from(
{ length: TABLE_SIZE },
(_, exponent) => DECIMAL_BASE ** exponent,
);
const MAX_EXACT_INTEGER = Number.MAX_SAFE_INTEGER + 1;
/**
* Rounds a number to a number of decimals, half away from zero, with plain arithmetic: the same result as
* `Number(formatDecimal(value, maxFractionDigits))`, much faster. Never returns `-0`.
*
* @param value - The number to round; `NaN` and infinities come back unchanged.
* @param maxFractionDigits - The number of decimals to keep. Defaults to `3`.
* @returns The rounded number.
* @example
* roundToFractionDigits(1.005, 2); // 1.01 (`toFixed` gives 1.00)
* roundToFractionDigits(-2.5, 0); // -3
*/
export function roundToFractionDigits(value: number, maxFractionDigits = 3): number {
const factor = POWERS_OF_TEN[maxFractionDigits] ?? DECIMAL_BASE ** maxFractionDigits;
const scaled = Math.abs(value) * factor;
if (!Number.isFinite(scaled) || scaled >= MAX_EXACT_INTEGER) {
return value;
}
// `1 + EPSILON` absorbs the binary error: 1.005 is stored as 1.00499999999999989…
const rounded = Math.round(scaled * (1 + Number.EPSILON)) / factor;
return rounded !== 0 && value < 0 ? -rounded : rounded;
}Test file
ts
import { formatDecimal } from '../format';
import { roundToFractionDigits } from './round-to-fraction-digits';
describe(roundToFractionDigits, () => {
it.for([
[1.005, 2, 1.01],
[1.004, 2, 1],
[-1.005, 2, -1.01],
[2.5, 0, 3],
[-2.5, 0, -3],
[0.285, 2, 0.29],
[1.45, 1, 1.5],
[123.456, 1, 123.5],
[1e21, 2, 1e21],
[0.0000001, 8, 0.0000001],
[1.5, 150, 1.5],
] as const)('rounds %s to %s digits as %s', ([value, digits, expected]) => {
expect(roundToFractionDigits(value, digits)).toBe(expected);
});
it('never returns -0', () => {
expect(Object.is(roundToFractionDigits(-0.001, 2), 0)).toBe(true);
expect(Object.is(roundToFractionDigits(-0, 2), 0)).toBe(true);
});
it('returns non-finite values unchanged', () => {
expect(roundToFractionDigits(NaN, 2)).toBeNaN();
expect(roundToFractionDigits(Infinity, 2)).toBe(Infinity);
});
it('matches formatDecimal on decimal inputs', () => {
const mismatches: string[] = [];
for (let digits = 0; digits <= 4; digits++) {
for (let thousandths = -20_000; thousandths <= 20_000; thousandths += 7) {
const value = thousandths / 1000;
const expected = Number(formatDecimal(value, digits));
if (roundToFractionDigits(value, digits) !== expected) {
mismatches.push(`${value} (${digits} digits)`);
}
}
}
expect(mismatches).toStrictEqual([]);
});
it('takes the defaults', () => {
expect(roundToFractionDigits(Math.PI)).toStrictEqual(roundToFractionDigits(Math.PI, 3));
});
});Keywords: round, to, fraction, digits, math, function