Skip to content

Commit

Permalink
Round func updates (#1917)
Browse files Browse the repository at this point in the history
* Remove trailing zeroes from round func

* exponential threshold

* decimaljs support

* test fixes

* exp notation negative tests
  • Loading branch information
grod220 authored Nov 21, 2024
1 parent ccbe3a5 commit 838de8a
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/slow-cats-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@penumbra-zone/types': patch
---

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
91 changes: 66 additions & 25 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 @@ -26,90 +26,131 @@ describe('round function', () => {
{
description: 'should round zero',
options: { value: 0, decimals: 2 },
expected: '0.00',
expected: '0',
},
{
description: 'should round an integer with decimals',
options: { value: 5, decimals: 2 },
expected: '5.00',
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',
},
// Exponential Notation Test Cases
{
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 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',
},
{
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 small negative numbers with floor mode',
options: { value: -5.770789431026099e23, decimals: 4, roundingMode: 'down' },
expected: '-5.7707e+23',
},
{
description: 'should handle extremely small negative numbers with ceil mode',
options: { value: -5.770789431026099e23, decimals: 4, roundingMode: 'up' },
expected: '-5.7708e+23',
},
// Edge Cases
{
description: 'should handle very large numbers',
options: { value: 1.23456789e10, decimals: 2, roundingMode: 'round' },
expected: '12345678900.00',
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: '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' },
expected: '1.20000',
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
66 changes: 42 additions & 24 deletions packages/types/src/round.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,57 @@
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');

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);
return roundedNumber.toFixed(decimals);
export function round({ value, decimals, roundingMode = 'half-up' }: RoundOptions): string {
const decimalValue = new Decimal(value);

// 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();

let result: string;

if (isLargeNumber || isSmallNumber) {
result = decimalValue.toExponential(decimals, getDecimalRoundingMode(roundingMode));
} else {
const roundedDecimal = decimalValue.toDecimalPlaces(
decimals,
getDecimalRoundingMode(roundingMode),
);
result = roundedDecimal.toFixed(decimals, getDecimalRoundingMode(roundingMode));
}

return removeTrailingZeros(result);
}
5 changes: 1 addition & 4 deletions packages/types/src/shortify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ export const removeTrailingZeros = (str: string): string => {
* - 1_000_000_000_000 -> 1T
*/
export const shortify = (value: number): string => {
// Rounding toward zero
const roundingMode = value >= 0 ? 'floor' : 'ceil';

let shortValue: number;
let suffix = '';
let decimals = 0;
Expand Down Expand Up @@ -63,7 +60,7 @@ export const shortify = (value: number): string => {
const roundedShortValueStr = round({
value: shortValue,
decimals: decimals,
roundingMode,
roundingMode: 'down', // Rounding toward zero
});

// Remove trailing zeros and append the suffix
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 838de8a

Please sign in to comment.