diff --git a/.changeset/slow-cats-punch.md b/.changeset/slow-cats-punch.md index df2643aef..a8d0ed2ca 100644 --- a/.changeset/slow-cats-punch.md +++ b/.changeset/slow-cats-punch.md @@ -2,4 +2,4 @@ '@penumbra-zone/types': patch --- -Removing trailing zeroes from round func +Round func updates: remove trailing zeros + exponent notation support diff --git a/packages/types/package.json b/packages/types/package.json index a6d978d29..115911e3d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -36,6 +36,7 @@ }, "dependencies": { "bignumber.js": "^9.1.2", + "decimal.js": "^10.4.3", "idb": "^8.0.0", "lodash": "^4.17.21", "zod": "^3.23.8" diff --git a/packages/types/src/round.test.ts b/packages/types/src/round.test.ts index d2cc94948..7cdfdaaae 100644 --- a/packages/types/src/round.test.ts +++ b/packages/types/src/round.test.ts @@ -7,7 +7,7 @@ describe('round function', () => { options: RoundOptions; expected: string; }[] = [ - // Default rounding mode ('round') + // Default rounding mode ('half-up') { description: 'should round up using default rounding (round)', options: { value: 1.2345, decimals: 3 }, @@ -33,93 +33,109 @@ describe('round function', () => { options: { value: 5, decimals: 2 }, expected: '5', }, - // Rounding mode: 'ceil' + // Rounding mode: 'up' { description: 'should ceil to 2 decimals', - options: { value: 1.2345, decimals: 2, roundingMode: 'ceil' }, + options: { value: 1.2345, decimals: 2, roundingMode: 'up' }, expected: '1.24', }, { description: 'should ceil a negative number', - options: { value: -1.2345, decimals: 2, roundingMode: 'ceil' }, - expected: '-1.23', + options: { value: -1.2345, decimals: 2, roundingMode: 'up' }, + expected: '-1.24', }, { description: 'should ceil with zero decimals', - options: { value: 1.5, decimals: 0, roundingMode: 'ceil' }, + options: { value: 1.5, decimals: 0, roundingMode: 'up' }, expected: '2', }, - // Rounding mode: 'floor' + // Rounding mode: 'down' { description: 'should floor to 2 decimals', - options: { value: 1.2399, decimals: 2, roundingMode: 'floor' }, + options: { value: 1.2399, decimals: 2, roundingMode: 'down' }, expected: '1.23', }, { description: 'should floor a negative number', - options: { value: -1.2345, decimals: 2, roundingMode: 'floor' }, - expected: '-1.24', + options: { value: -1.2345, decimals: 2, roundingMode: 'down' }, + expected: '-1.23', }, { description: 'should floor with zero decimals', - options: { value: 1.9, decimals: 0, roundingMode: 'floor' }, + options: { value: 1.9, decimals: 0, roundingMode: 'down' }, expected: '1', }, - // Edge Cases + // Exponential Notation Test Cases { - description: 'should handle large numbers', - options: { value: 1.23456789e10, decimals: 4, roundingMode: 'round' }, - expected: '12345678900', + description: 'should handle extremely large numbers with round mode', + options: { value: 5.770789431026099e23, decimals: 4, roundingMode: 'half-up' }, + expected: '5.7708e+23', }, { - description: 'should handle extremely large numbers', - options: { value: 5.770789431026099e23, decimals: 4, roundingMode: 'round' }, + description: 'should handle extremely large numbers with floor mode', + options: { value: 5.770789431026099e23, decimals: 4, roundingMode: 'down' }, + expected: '5.7707e+23', + }, + { + description: 'should handle extremely large numbers with ceil mode', + options: { value: 5.770789431026099e23, decimals: 4, roundingMode: 'up' }, expected: '5.7708e+23', }, + { + description: 'should handle extremely large negative numbers', + options: { value: -5.770789431026099e23, decimals: 4, roundingMode: 'half-up' }, + expected: '-5.7708e+23', + }, + // Edge Cases + { + description: 'should handle large numbers', + options: { value: 1.23456789e10, decimals: 4, roundingMode: 'half-up' }, + expected: '12345678900', + }, { description: 'should remove trailing zeros', - options: { value: 1.0000000001, decimals: 4, roundingMode: 'round' }, + options: { value: 1.0000000001, decimals: 4, roundingMode: 'half-up' }, expected: '1', }, { description: 'should handle very small numbers', - options: { value: 0.000123456, decimals: 8, roundingMode: 'round' }, + options: { value: 0.000123456, decimals: 8, roundingMode: 'half-up' }, expected: '0.00012346', }, { description: 'should handle Infinity', - options: { value: Infinity, decimals: 2, roundingMode: 'round' }, + options: { value: Infinity, decimals: 2, roundingMode: 'half-up' }, expected: 'Infinity', }, { description: 'should handle -Infinity', - options: { value: -Infinity, decimals: 2, roundingMode: 'floor' }, + options: { value: -Infinity, decimals: 2, roundingMode: 'down' }, expected: '-Infinity', }, { description: 'should handle NaN', - options: { value: NaN, decimals: 2, roundingMode: 'ceil' }, + options: { value: NaN, decimals: 2, roundingMode: 'up' }, expected: 'NaN', }, { description: 'should handle decimals greater than available decimal places', - options: { value: 1.2, decimals: 5, roundingMode: 'floor' }, + options: { value: 1.2, decimals: 5, roundingMode: 'down' }, expected: '1.2', }, // Rounding to integer { description: 'should round to integer using round mode', - options: { value: 2.5, decimals: 0, roundingMode: 'round' }, + options: { value: 2.5, decimals: 0, roundingMode: 'half-up' }, expected: '3', }, { description: 'should ceil to integer', - options: { value: 2.1, decimals: 0, roundingMode: 'ceil' }, + options: { value: 2.1, decimals: 0, roundingMode: 'up' }, expected: '3', }, { description: 'should floor to integer', - options: { value: 2.9, decimals: 0, roundingMode: 'floor' }, + options: { value: 2.9, decimals: 0, roundingMode: 'down' }, expected: '2', }, ]; diff --git a/packages/types/src/round.ts b/packages/types/src/round.ts index ef5589fb3..f80282a5a 100644 --- a/packages/types/src/round.ts +++ b/packages/types/src/round.ts @@ -1,7 +1,7 @@ -import { ceil as lodashCeil, floor as lodashFloor, round as lodashRound } from 'lodash'; +import { Decimal } from 'decimal.js'; import { removeTrailingZeros } from './shortify.js'; -export type RoundingMode = 'round' | 'ceil' | 'floor'; +export type RoundingMode = 'half-up' | 'up' | 'down'; export interface RoundOptions { value: number; @@ -9,45 +9,48 @@ export interface RoundOptions { roundingMode?: RoundingMode; } -const roundingStrategies = { - ceil: lodashCeil, - floor: lodashFloor, - round: lodashRound, -} as const; +const EXPONENTIAL_NOTATION_THRESHOLD = new Decimal('1e21'); -const EXPONENTIAL_NOTATION_THRESHOLD = 1e21; +Decimal.set({ precision: 30 }); + +const getDecimalRoundingMode = (mode: RoundingMode): Decimal.Rounding => { + switch (mode) { + case 'up': + return Decimal.ROUND_UP; + case 'down': + return Decimal.ROUND_DOWN; + case 'half-up': + default: + return Decimal.ROUND_HALF_UP; + } +}; /** - * Rounds a number based on the specified options. - * * @param options - An object containing the properties: * - value: The number to round. * - decimals: The number of decimal places to round to. - * - roundingMode: The mode of rounding ('round', 'ceil', 'floor'). Defaults to 'round'. - * - * @returns A string representation of the rounded number. - * - * @example - * - * ```typescript - * round({ value: 1.2345, decimals: 2, roundingMode: 'ceil' }); // "1.24" - * round({ value: 1.2345, decimals: 2, roundingMode: 'floor' }); // "1.23" - * round({ value: 1.2345, decimals: 2 }); // "1.23" (default rounding) - * ``` + * - roundingMode: + * - half-up: Default. Rounds towards nearest neighbour. If equidistant, rounds away from zero. + * - down: Rounds towards zero + * - up: Rounds way from zero */ -export function round({ value, decimals, roundingMode = 'round' }: RoundOptions): string { - const roundingFn = roundingStrategies[roundingMode]; - const roundedNumber = roundingFn(value, decimals); +export function round({ value, decimals, roundingMode = 'half-up' }: RoundOptions): string { + const decimalValue = new Decimal(value); - let result: string; + // Determine if exponential notation is needed + const isLargeNumber = decimalValue.abs().gte(EXPONENTIAL_NOTATION_THRESHOLD); + const isSmallNumber = decimalValue.abs().lt(new Decimal('1e-4')) && !decimalValue.isZero(); - const isLargeNumber = Math.abs(roundedNumber) >= EXPONENTIAL_NOTATION_THRESHOLD; - const isSmallNumber = Math.abs(roundedNumber) < 1e-4 && roundedNumber !== 0; + let result: string; if (isLargeNumber || isSmallNumber) { - result = roundedNumber.toExponential(decimals); + result = decimalValue.toExponential(decimals, getDecimalRoundingMode(roundingMode)); } else { - result = roundedNumber.toFixed(decimals); + const roundedDecimal = decimalValue.toDecimalPlaces( + decimals, + getDecimalRoundingMode(roundingMode), + ); + result = roundedDecimal.toFixed(decimals, getDecimalRoundingMode(roundingMode)); } return removeTrailingZeros(result); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 775e93d3f..0a22f9f9b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -595,6 +595,9 @@ importers: bignumber.js: specifier: ^9.1.2 version: 9.1.2 + decimal.js: + specifier: ^10.4.3 + version: 10.4.3 idb: specifier: ^8.0.0 version: 8.0.0