Skip to content

Commit

Permalink
decimaljs support
Browse files Browse the repository at this point in the history
  • Loading branch information
grod220 committed Nov 20, 2024
1 parent 39e67f5 commit 0dbb840
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .changeset/slow-cats-punch.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@penumbra-zone/types': patch
---

Removing trailing zeroes from round func
Round func updates: remove trailing zeros + exponent notation support
1 change: 1 addition & 0 deletions packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
68 changes: 42 additions & 26 deletions packages/types/src/round.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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',
},
];
Expand Down
61 changes: 32 additions & 29 deletions packages/types/src/round.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,56 @@
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;
decimals: number;
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);
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0dbb840

Please sign in to comment.