From 9bc58de1faf9d60d108bfed54dd3557b3bab01de Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Mon, 6 Feb 2023 12:31:50 +0000 Subject: [PATCH] improved rounding method for decimals --- src/dnum.ts | 39 +++++++++++++++++++++++++++++---------- src/utils.ts | 10 ++++++++++ test/all.test.ts | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/dnum.ts b/src/dnum.ts index ff80b1f..5015177 100644 --- a/src/dnum.ts +++ b/src/dnum.ts @@ -1,6 +1,12 @@ import type { Decimals, Dnum, Numberish, Value } from "./types"; -import { divideAndRound, powerOfTen, splitNumber } from "./utils"; +import { + abs, + divideAndRound, + powerOfTen, + roundToPower, + splitNumber, +} from "./utils"; export function isDnum(value: unknown): value is Dnum { return ( @@ -126,7 +132,7 @@ export function toParts( ): [whole: bigint, fraction: string | null] { const [value, decimals] = dnum; - // options.digits can also be passed directly as the third argument + // options.digits can also be passed directly as the second argument if (typeof optionsOrDigits === "number") { optionsOrDigits = { digits: optionsOrDigits }; } @@ -143,16 +149,29 @@ export function toParts( const decimalsDivisor = powerOfTen(decimals); const whole = value / decimalsDivisor; - let fraction = String(value % decimalsDivisor).replace(/^-/, ""); - - const zeros = "0".repeat( - Math.max(0, String(decimalsDivisor).length - fraction.length - 1), + const fractionValue = abs(value % decimalsDivisor); + + let fraction = String( + roundToPower( + BigInt( + // prefix with 1 to keep the leading zeros + "1" + // leading zeros + + "0".repeat( + Math.max( + 0, + String(decimalsDivisor).length - String(fractionValue).length - 1, + ), + ) + // non zero numbers + + String(fractionValue), + ), + powerOfTen(Math.max(0, decimals - digits)), + ), ); - fraction = zeros + divideAndRound( - BigInt(fraction), - powerOfTen(Math.max(0, decimals - digits)), - ); + // remove the leading 1 and extra decimal places + fraction = fraction.slice(1, digits + 1); if (trailingZeros) { fraction = fraction.padEnd(digits, "0"); diff --git a/src/utils.ts b/src/utils.ts index 218cd59..7651d87 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,3 +21,13 @@ export function powerOfTen(zeroes: number) { // See https://github.com/codesandbox/codesandbox-client/issues/6706 return BigInt("1" + "0".repeat(zeroes)); } + +export function roundToPower(value: bigint, power: bigint) { + const a = (value / power) * power; + const b = a + power; + return (value - a >= b - value) ? b : a; +} + +export function abs(value: bigint) { + return value < BigInt(0) ? -value : value; +} diff --git a/test/all.test.ts b/test/all.test.ts index 13b0675..54a9f61 100644 --- a/test/all.test.ts +++ b/test/all.test.ts @@ -460,6 +460,42 @@ describe("toParts()", () => { [0n, "00001"], ); }); + it("rounds decimals properly", () => { + expect(toParts([49999999n, 9], 1)).toEqual([0n, null]); + expect(toParts([50000000n, 9], 1)).toEqual([0n, "1"]); + expect(toParts([49998805n, 9], 2)).toEqual([0n, "05"]); + expect(toParts([90000000n, 9], 1)).toEqual([0n, "1"]); + + // 1234.56 + expect(toParts([123456n, 2], 1)).toEqual([1234n, "6"]); + expect(toParts([123456n, 2], 2)).toEqual([1234n, "56"]); + expect(toParts([123456n, 2], 3)).toEqual([1234n, "56"]); + + // 0.09 + expect(toParts([9n, 2], 1)).toEqual([0n, "1"]); + expect(toParts([9n, 2], 2)).toEqual([0n, "09"]); + + // 1.09 + expect(toParts([109n, 2], 1)).toEqual([1n, "1"]); + expect(toParts([109n, 2], 2)).toEqual([1n, "09"]); + + // 0.006 + expect(toParts([6n, 3], 1)).toEqual([0n, null]); + expect(toParts([6n, 3], 2)).toEqual([0n, "01"]); + expect(toParts([6n, 3], 3)).toEqual([0n, "006"]); + + // 0.049998805; + expect(toParts([49998805n, 9], 0)).toEqual([0n, null]); + expect(toParts([49998805n, 9], 1)).toEqual([0n, null]); + expect(toParts([49998805n, 9], 2)).toEqual([0n, "05"]); + expect(toParts([49998805n, 9], 3)).toEqual([0n, "05"]); + expect(toParts([49998805n, 9], 4)).toEqual([0n, "05"]); + expect(toParts([49998805n, 9], 5)).toEqual([0n, "05"]); + expect(toParts([49998805n, 9], 6)).toEqual([0n, "049999"]); + expect(toParts([49998805n, 9], 7)).toEqual([0n, "0499988"]); + expect(toParts([49998805n, 9], 8)).toEqual([0n, "04999881"]); + expect(toParts([49998805n, 9], 9)).toEqual([0n, "049998805"]); + }); }); describe("toNumber()", () => {