Skip to content

Commit

Permalink
improve subscript function (#275)
Browse files Browse the repository at this point in the history
* prepare todo

* lock

* make changes

* update comment

* revert lock file

* new format

* suggestion

* Update utils/numbersFormatting.ts

Co-authored-by: Victor Kirov <victor.kirov@gmail.com>

* Update utils/numbersFormatting.ts

Co-authored-by: Victor Kirov <victor.kirov@gmail.com>

* suggestions

* suggestions!

* Update utils/numbersFormatting.ts

Co-authored-by: Victor Kirov <victor.kirov@gmail.com>

---------

Co-authored-by: Victor Kirov <victor.kirov@gmail.com>
  • Loading branch information
terencehh and victorkirov authored Dec 5, 2024
1 parent a65c47a commit fb8309e
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 57 deletions.
134 changes: 113 additions & 21 deletions tests/utils/numbersFormatting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,133 @@ import { describe, it, expect } from 'vitest';
import { formatBalance } from '../../utils/numbersFormatting';

describe('formatBalance', () => {
it('should handle normal decimals with rounding to 6 places', () => {
expect(formatBalance('123.4567891')).toBe('123.456789');
expect(formatBalance('123.456789923')).toBe('123.45679');
expect(formatBalance('123.45')).toBe('123.45');
expect(formatBalance('0.999999999')).toBe('1.0');
it('should handle normal decimals up to 6 places', () => {
expect(formatBalance('123.4567891')).toEqual({
prefix: '123.456789',
isRounded: true,
});
expect(formatBalance('123.456789923')).toEqual({
prefix: '123.456789',
isRounded: true,
});
expect(formatBalance('123.45')).toEqual({
prefix: '123.45',
isRounded: false,
});
expect(formatBalance('0.999999999')).toEqual({
prefix: '0.999999',
isRounded: true,
});
});

it('should handle numbers with up to 3 leading zeros showing 6 decimals', () => {
expect(formatBalance('0.0001234567')).toBe('0.000123');
expect(formatBalance('0.000123456789')).toBe('0.000123');
expect(formatBalance('123.000456789')).toBe('123.000457');
expect(formatBalance('0.0001234567')).toEqual({
prefix: '0.000123',
isRounded: true,
});
expect(formatBalance('0.000123456789')).toEqual({
prefix: '0.000123',
isRounded: true,
});
expect(formatBalance('123.000456789')).toEqual({
prefix: '123.000456',
isRounded: true,
});
expect(formatBalance('123.000456')).toEqual({
prefix: '123.000456',
isRounded: false,
});
});

it('should use subscript notation for 4 or more leading zeros with 4 significant digits', () => {
expect(formatBalance('0.00001234')).toBe('0.0₄1234');
expect(formatBalance('0.00001')).toBe('0.0₄1000');
expect(formatBalance('0.000001234567')).toBe('0.0₅1235');
expect(formatBalance('0.00000001234567')).toBe('0.0₇1235');
expect(formatBalance('123.00000001234')).toBe('123.0₇1234');
expect(formatBalance('0.00001234')).toEqual({
prefix: '0.0',
suffix: {
subscript: '4',
value: '1234',
},
isRounded: false,
});
expect(formatBalance('0.00001')).toEqual({
prefix: '0.0',
suffix: {
subscript: '4',
value: '1',
},
isRounded: false,
});
expect(formatBalance('0.000001234567')).toEqual({
prefix: '0.0',
suffix: {
subscript: '5',
value: '1234',
},
isRounded: true,
});
expect(formatBalance('0.00000001234567')).toEqual({
prefix: '0.0',
suffix: {
subscript: '7',
value: '1234',
},
isRounded: true,
});
expect(formatBalance('123.00000001234')).toEqual({
prefix: '123.0',
suffix: {
subscript: '7',
value: '1234',
},
isRounded: false,
});
});

it('should handle all zeros after decimal point', () => {
expect(formatBalance('123.000000000')).toBe('123.0₉');
expect(formatBalance('0.000000000')).toBe('0.0₉');
expect(formatBalance('123.000000000')).toEqual({
prefix: '123',
isRounded: false,
});
expect(formatBalance('0.000000000')).toEqual({
prefix: '0',
isRounded: false,
});
});

it('should handle large numbers of leading zeros', () => {
expect(formatBalance('0.000000000000006789')).toBe('0.0₁₄6789');
expect(formatBalance('0.' + '0'.repeat(99999) + '6789')).toBe('0.0₉₉₉₉₉6789');
expect(formatBalance('0.000000000000006789')).toEqual({
prefix: '0.0',
suffix: {
subscript: '14',
value: '6789',
},
isRounded: false,
});
expect(formatBalance('0.' + '0'.repeat(99999) + '6789')).toEqual({
prefix: '0.0',
suffix: {
subscript: '99999',
value: '6789',
},
isRounded: false,
});
});

it('should handle numbers with commas', () => {
expect(formatBalance('1234.4567891')).toBe('1,234.456789');
expect(formatBalance('300000')).toBe('300,000');
expect(formatBalance('300,000')).toBe('300,000');
expect(formatBalance('198,867.33334')).toBe('198,867.33334');
expect(formatBalance('1234.4567891')).toEqual({
prefix: '1,234.456789',
isRounded: true,
});
expect(formatBalance('300000')).toEqual({
prefix: '300,000',
isRounded: false,
});
expect(formatBalance('300,000')).toEqual({
prefix: '300,000',
isRounded: false,
});
expect(formatBalance('198,867.33334')).toEqual({
prefix: '198,867.33334',
isRounded: false,
});
});
});
83 changes: 47 additions & 36 deletions utils/numbersFormatting.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import BigNumber from 'bignumber.js';

/**
* Formats a given value as a localized number string.
* If the value is undefined or null, returns a dash ('-').
Expand All @@ -6,62 +8,71 @@
* @returns The formatted number string or a dash if the value is not provided.
*/
export const formatNumber = (value?: string | number) => {
if (!value) return '-';
if (!value && value !== 0) return '-';
const cleanValue = typeof value === 'string' ? value.replace(/,/g, '') : value;
return new Intl.NumberFormat().format(Number(cleanValue));
};

export interface FormattedBalance {
prefix: string;
suffix?: {
subscript: string;
value: string;
};
isRounded: boolean;
}

/**
* Formats a string which may contain many extremely large values or extremely high decimal places
* Numbers with >= 4 leading zeros will return subscript notation.
* Limits decimal places to 6 digits after removing leading zeros, with proper rounding.
* For regular decimals without leading zeros, limits to 6 decimal places with rounding.
* Limits decimal places to 6 digits after removing leading zeros.
* For regular decimals without leading zeros, limits to 6 decimal places.
* any significant digit over 6 decimal places will be rounded down for safety.
*
* @param value - The number string to format.
* @returns The formatted number string.
*/
export const formatBalance = (value: string): string => {
// Unicode subscript characters for numbers
const unicodeSubChars = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'];
export const formatBalance = (value: string): FormattedBalance => {
const [integerPart, decimalPartRaw] = value.split('.');
// remove zeros at end of raw decimal part
const decimalPart = decimalPartRaw?.replace(/0+$/, '');

const [integerPart, decimalPart] = value.split('.');

// If there's no decimal part, return the original value
if (!decimalPart) return formatNumber(value);
// If there's no decimal part or it's all zeros, return just the formatted integer
if (!decimalPart || /^0+$/.test(decimalPart)) {
return {
prefix: formatNumber(integerPart),
isRounded: false,
};
}

const leadingZerosMatch = decimalPart.match(/^0+/);
const leadingZerosCount = leadingZerosMatch ? leadingZerosMatch[0].length : 0;

// If there are 3 or fewer leading zeros, or no leading zeros,
// truncate to 6 decimal places with rounding
// Handle regular decimals (3 or fewer leading zeros)
if (leadingZerosCount <= 3) {
const roundedNum = Math.round(Number(`0.${decimalPart}`) * 1000000) / 1000000;
if (roundedNum === 1) {
return `${formatNumber(Number(integerPart) + 1)}.0`;
}
const roundedParts = roundedNum.toString().split('.');
return `${formatNumber(integerPart)}.${roundedParts[1] || '0'}`;
}
const initialNumber = BigNumber(`0.${decimalPart}`);
const flooredNum = initialNumber.dp(6, BigNumber.ROUND_FLOOR);
const flooredParts = flooredNum.toString().split('.');

// Handle cases with more than 3 leading zeros
const countString = leadingZerosCount.toString();
const truncatedCount = countString.slice(0, 5);
const leadingZerosUnicode = truncatedCount
.split('')
.map((digit) => unicodeSubChars[parseInt(digit, 10)])
.join('');
return {
prefix: `${formatNumber(integerPart)}.${flooredParts[1] || '0'}`,
isRounded: initialNumber.gt(flooredNum),
};
}

// Handle cases with 4 or more leading zeros
const remainingDecimalPart = decimalPart.slice(leadingZerosCount);

// Check if all remaining digits are zeros
if (/^0+$/.test(remainingDecimalPart) || !remainingDecimalPart) {
return `${formatNumber(integerPart)}.0${leadingZerosUnicode}`;
}

// Round to 4 decimal places properly for significant digits
const roundedValue = (Math.round(Number(`0.${remainingDecimalPart}`) * 10000) / 10000)
.toFixed(4)
.slice(2); // Remove "0." from the start
// Handle significant digits
const significantDigits = remainingDecimalPart.slice(0, 4);
const isRounded = remainingDecimalPart.length > 4;

return `${formatNumber(integerPart)}.0${leadingZerosUnicode}${roundedValue}`;
return {
prefix: `${formatNumber(integerPart)}.0`,
suffix: {
subscript: leadingZerosCount.toString(),
value: significantDigits,
},
isRounded,
};
};

0 comments on commit fb8309e

Please sign in to comment.