Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance toNano and fromNano functions to support customizable decimal precision #62

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 77 additions & 54 deletions src/utils/convert.spec.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,90 @@
/**
* Copyright (c) Whales Corp.
* Copyright (c) Whales Corp.
* All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import { fromNano, toNano } from "./convert";
import { fromNano, toNano } from './convert';

const stringCases: { nano: string, real: string }[] = [
{ real: '1', nano: '1000000000' },
{ real: '10', nano: '10000000000' },
{ real: '0.1', nano: '100000000' },
{ real: '0.33', nano: '330000000' },
{ real: '0.000000001', nano: '1' },
{ real: '10.000000001', nano: '10000000001' },
{ real: '1000000.000000001', nano: '1000000000000001' },
{ real: '100000000000', nano: '100000000000000000000' },
const stringCases: { nano: string; real: string }[] = [
{ real: '1', nano: '1000000000' },
{ real: '10', nano: '10000000000' },
{ real: '0.1', nano: '100000000' },
{ real: '0.33', nano: '330000000' },
{ real: '0.000000001', nano: '1' },
{ real: '10.000000001', nano: '10000000001' },
{ real: '1000000.000000001', nano: '1000000000000001' },
{ real: '100000000000', nano: '100000000000000000000' },
];

const numberCases: { nano: string, real: number }[] = [
{ real: -0, nano: '0' },
{ real: 0, nano: '0' },
{ real: 1e64, nano: '10000000000000000000000000000000000000000000000000000000000000000000000000' },
{ real: 1, nano: '1000000000' },
{ real: 10, nano: '10000000000' },
{ real: 0.1, nano: '100000000' },
{ real: 0.33, nano: '330000000' },
{ real: 0.000000001, nano: '1' },
{ real: 10.000000001, nano: '10000000001' },
{ real: 1000000.000000001, nano: '1000000000000001' },
{ real: 100000000000, nano: '100000000000000000000' },
const numberCases: { nano: string; real: number }[] = [
{ real: -0, nano: '0' },
{ real: 0, nano: '0' },
{
real: 1e64,
nano: '10000000000000000000000000000000000000000000000000000000000000000000000000',
},
{ real: 1, nano: '1000000000' },
{ real: 10, nano: '10000000000' },
{ real: 0.1, nano: '100000000' },
{ real: 0.33, nano: '330000000' },
{ real: 0.000000001, nano: '1' },
{ real: 10.000000001, nano: '10000000001' },
{ real: 1000000.000000001, nano: '1000000000000001' },
{ real: 100000000000, nano: '100000000000000000000' },
];

describe('convert', () => {
it('should throw an error for NaN', () => {
expect(() => toNano(NaN)).toThrow();
});
it('should throw an error for Infinity', () => {
expect(() => toNano(Infinity)).toThrow();
});
it('should throw an error for -Infinity', () => {
expect(() => toNano(-Infinity)).toThrow();
});
it('should throw an error due to insufficient precision of number', () => {
expect(() => toNano(10000000.000000001)).toThrow();
});
it('should convert numbers toNano', () => {
for (let r of numberCases) {
let c = toNano(r.real);
expect(c).toBe(BigInt(r.nano));
}
});
it('should convert strings toNano', () => {
for (let r of stringCases) {
let c = toNano(r.real);
expect(c).toBe(BigInt(r.nano));
}
});
it('should convert fromNano', () => {
for (let r of stringCases) {
let c = fromNano(r.nano);
expect(c).toEqual(r.real);
}
});
});
it('should throw an error for NaN', () => {
expect(() => toNano(NaN)).toThrow();
});
it('should throw an error for Infinity', () => {
expect(() => toNano(Infinity)).toThrow();
});
it('should throw an error for -Infinity', () => {
expect(() => toNano(-Infinity)).toThrow();
});
it('should throw an error due to insufficient precision of number', () => {
expect(() => toNano(10000000.000000001)).toThrow();
});
it('should convert numbers toNano', () => {
for (let r of numberCases) {
let c = toNano(r.real);
expect(c).toBe(BigInt(r.nano));
}
});
it('should convert strings toNano', () => {
for (let r of stringCases) {
let c = toNano(r.real);
expect(c).toBe(BigInt(r.nano));
}
});
it('should convert fromNano', () => {
for (let r of stringCases) {
let c = fromNano(r.nano);
expect(c).toEqual(r.real);
}
});
it('should convert numbers toNano with custom decimals', () => {
expect(toNano(1, 6)).toBe(BigInt('1000000'));
expect(toNano(0.1, 6)).toBe(BigInt('100000'));
expect(toNano(0.000001, 6)).toBe(BigInt('1'));
expect(toNano(10.000001, 6)).toBe(BigInt('10000001'));
});

it('should convert strings toNano with custom decimals', () => {
expect(toNano('1', 6)).toBe(BigInt('1000000'));
expect(toNano('0.1', 6)).toBe(BigInt('100000'));
expect(toNano('0.000001', 6)).toBe(BigInt('1'));
expect(toNano('10.000001', 6)).toBe(BigInt('10000001'));
});

it('should convert fromNano with custom decimals', () => {
expect(fromNano('1000000', 6)).toEqual('1');
expect(fromNano('100000', 6)).toEqual('0.1');
expect(fromNano('1', 6)).toEqual('0.000001');
expect(fromNano('10000001', 6)).toEqual('10.000001');
});
});
167 changes: 88 additions & 79 deletions src/utils/convert.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,105 @@
/**
* Copyright (c) Whales Corp.
* Copyright (c) Whales Corp.
* All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

export function toNano(src: number | string | bigint): bigint {
export function toNano(
src: number | string | bigint,
decimals: number = 9
): bigint {
if (typeof src === 'bigint') {
return src * 10n ** BigInt(decimals);
} else {
if (typeof src === 'number') {
if (!Number.isFinite(src)) {
throw Error('Invalid number');
}

if (typeof src === 'bigint') {
return src * 1000000000n;
} else {
if (typeof src === 'number') {
if (!Number.isFinite(src)) {
throw Error('Invalid number');
}
if (Math.log10(src) <= 6) {
src = src.toLocaleString('en', {
minimumFractionDigits: decimals,
useGrouping: false,
});
} else if (src - Math.trunc(src) === 0) {
src = src.toLocaleString('en', {
maximumFractionDigits: 0,
useGrouping: false,
});
} else {
throw Error(
'Not enough precision for a number value. Use string value instead'
);
}
}
// Check sign
let neg = false;
while (src.startsWith('-')) {
neg = !neg;
src = src.slice(1);
}

if (Math.log10(src) <= 6) {
src = src.toLocaleString('en', { minimumFractionDigits: 9, useGrouping: false });
} else if (src - Math.trunc(src) === 0) {
src = src.toLocaleString('en', { maximumFractionDigits: 0, useGrouping: false });
} else {
throw Error('Not enough precision for a number value. Use string value instead');
}
}
// Split string
if (src === '.') {
throw Error('Invalid number');
}
let parts = src.split('.');
if (parts.length > 2) {
throw Error('Invalid number');
}

// Check sign
let neg = false;
while (src.startsWith('-')) {
neg = !neg;
src = src.slice(1);
}
// Prepare parts
let whole = parts[0];
let frac = parts[1];
if (!whole) {
whole = '0';
}
if (!frac) {
frac = '0';
}
if (frac.length > decimals) {
throw Error('Invalid number');
}
while (frac.length < decimals) {
frac += '0';
}

// Split string
if (src === '.') {
throw Error('Invalid number');
}
let parts = src.split('.');
if (parts.length > 2) {
throw Error('Invalid number');
}

// Prepare parts
let whole = parts[0];
let frac = parts[1];
if (!whole) {
whole = '0';
}
if (!frac) {
frac = '0';
}
if (frac.length > 9) {
throw Error('Invalid number');
}
while (frac.length < 9) {
frac += '0';
}

// Convert
let r = BigInt(whole) * 1000000000n + BigInt(frac);
if (neg) {
r = -r;
}
return r;
}
// Convert
let r = BigInt(whole) * 10n ** BigInt(decimals) + BigInt(frac);
if (neg) {
r = -r;
}
return r;
}
}

export function fromNano(src: bigint | number | string) {
let v = BigInt(src);
let neg = false;
if (v < 0) {
neg = true;
v = -v;
}
export function fromNano(src: bigint | number | string, decimals: number = 9) {
let v = BigInt(src);
let neg = false;
if (v < 0) {
neg = true;
v = -v;
}

// Convert fraction
let frac = v % 1000000000n;
let facStr = frac.toString();
while (facStr.length < 9) {
facStr = '0' + facStr;
}
facStr = facStr.match(/^([0-9]*[1-9]|0)(0*)/)![1];
// Convert fraction
let frac = v % 10n ** BigInt(decimals);
let facStr = frac.toString();
while (facStr.length < decimals) {
facStr = '0' + facStr;
}
facStr = facStr.match(/^([0-9]*[1-9]|0)(0*)/)![1];

// Convert whole
let whole = v / 1000000000n;
let wholeStr = whole.toString();
// Convert whole
let whole = v / 10n ** BigInt(decimals);
let wholeStr = whole.toString();

// Value
let value = `${wholeStr}${facStr === '0' ? '' : `.${facStr}`}`;
if (neg) {
value = '-' + value;
}
// Value
let value = `${wholeStr}${facStr === '0' ? '' : `.${facStr}`}`;
if (neg) {
value = '-' + value;
}

return value;
}
return value;
}