Skip to content
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
144 changes: 144 additions & 0 deletions src/currency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
export type Separators = {
int: string
decimal: string
point: string
}
export type Precisions = {
int: number
decimal: number
}

export type Currency = string
export const ERR_INVALID_TYPE = '"num" has invalid type'
export const ERR_INTEGER_LIMIT = '"num" is greater then MAX_SAFE_INTEGER, consider using string or BigInt'
export const ERR_INVALID_STRING = '"num" is invalid string, valid pattern is "100.100" or "100,100"'

export function toCurrency(
num: string | number | BigInt,
currency: string = toCurrency.currency,
precision: Precisions = toCurrency.precision,
separators: Separators = toCurrency.separators
): Currency {
let int = 0n
let decimal = 0n
let parts: [string, string] = ['', '']
switch (typeof num) {
case 'bigint':
num = BigInt(num)
break
case 'number':
if (num > Number.MAX_SAFE_INTEGER) {
throw new Error(ERR_INTEGER_LIMIT)
}
num = String(num)
case 'string':
switch (true) {
case /^\d+$/.test(num):
parts[0] = num
case /^\d+\.\d+$/.test(num):
parts = num.split('.') as [string, string]
break
case /^\d+\,\d+$/.test(num):
parts = num.split(',') as [string, string]
break
default:
throw new Error(ERR_INVALID_STRING)
}
int = BigInt(parts[0])
if (!parts[1]) parts[1] = '0'
decimal = BigInt(parts[1])
formatDecimal.initial = decimal
break

default:
throw new Error(ERR_INVALID_TYPE)
}
if (decimal !== 0n) {
decimal = roundInt(decimal, precision.decimal)
if (precision.int !== 0) {
if (decimal >= 5) {
int += 1n
decimal = 0n
}
}
}
int = roundInt(int, precision.int)
return `${currency}${formatInt(int, separators.int)}${separators.point}${formatDecimal(
decimal,
separators.decimal,
precision.decimal
)}`
}
toCurrency.currency = '$'
toCurrency.precision = { int: 0, decimal: 2 }
toCurrency.separators = { int: '.', point: ',', decimal: ' ' }

export function roundInt(n: BigInt, precision: number) {
let m = Array.from(n.toString(), s => Number(s))
precision = precision >= 0 ? precision : m.length
let counter = 0
let summand = 0
let i = m.length - 1
for (; i >= 0; i--) {
m[i] += summand
if (m[i] >= 5) {
summand = 1
} else {
summand = 0
}
if (counter >= precision - 1) {
break
} else {
if (summand) {
setPreviousZeros(m, i)
m[i] = 0
}
}
counter++
}
if (i > 0 && summand) {
setPreviousZeros(m, i)
m[i] = 0
}
return BigInt(m.join(''))
}

export function setPreviousZeros(nums: number[], idx: number) {
for (let i = idx; i < nums.length; i++) {
nums[i] = 0
}
return nums
}

export function formatInt(num: BigInt, separator: string) {
return format(num.toString(), separator)
}
export function formatDecimal(num: BigInt, separator: string, precision: number) {
let s = ''
precision = precision >= 0 ? precision : formatDecimal.initial.toString().length
if (num === 0n) {
s = '0'.repeat(precision || 1)
} else {
s = num.toString()
}
return format(s, separator)
}
formatDecimal.initial = 0n

export function format(s: string, separator: string) {
let arr = Array.from(s)
let buffer = []
let counter = 1
for (let i = arr.length - 1; i >= 0; i--) {
buffer.push(arr[i])
if (counter % 3 === 0 && i !== 0) {
buffer.push(separator)
}
counter++
}
return buffer.reverse().join('').trim()
}

// export function fromCurrency(value: Currency): BigInt {
// return 0n
// }
96 changes: 96 additions & 0 deletions test/currency.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// import { toCurrency, ERR_INVALID_TYPE, ERR_INTEGER_LIMIT, ERR_INVALID_STRING, roundInt } from '../src/currency'
import {
ERR_INTEGER_LIMIT,
ERR_INVALID_STRING,
ERR_INVALID_TYPE,
formatDecimal,
formatInt,
roundInt,
toCurrency
} from '../src/currency'

function testFormat(fn: (n: BigInt, s: string, ...args: any[]) => string) {
it('should return proper value', () => {
expect(fn(1000n, ' ', 0)).toEqual('1 000')
expect(fn(10000n, ' ', 0)).toEqual('10 000')
expect(fn(100000n, ' ', 0)).toEqual('100 000')
expect(fn(1000000n, ' ', 0)).toEqual('1 000 000')
})
}

describe('formatInt', () => {
it('should return proper value for zeros', () => {
expect(formatInt(0n, ' ')).toEqual('0')
})
testFormat(formatInt)
})

describe('formatDecimal', () => {
it('should return proper value for zeros', () => {
expect(formatDecimal(0n, ' ', -1)).toEqual('0')
expect(formatDecimal(0n, ' ', 0)).toEqual('0')
expect(formatDecimal(0n, ' ', 1)).toEqual('0')
expect(formatDecimal(0n, ' ', 2)).toEqual('00')
expect(formatDecimal(0n, ' ', 3)).toEqual('000')
expect(formatDecimal(0n, ' ', 4)).toEqual('0 000')
expect(formatDecimal(0n, ' ', 5)).toEqual('00 000')
expect(formatDecimal(0n, ' ', 6)).toEqual('000 000')
expect(formatDecimal(0n, ' ', 7)).toEqual('0 000 000')
})
testFormat(formatDecimal)
})

describe('roundInt', () => {
it('should return proper value for different values', () => {
expect(roundInt(445n, 3)).toEqual(500n)
expect(roundInt(9945n, 2)).toEqual(9900n)
expect(roundInt(99454n, 3)).toEqual(99000n)
expect(roundInt(190000000000453n, 15)).toEqual(200000000000000n)
expect(roundInt(190000000000453n, 0)).toEqual(190000000000453n)
})
it('should return proper value with negative precision', () => {
expect(roundInt(190000000000453n, -1)).toEqual(200000000000000n)
})
})

describe('toCurrency', () => {
it('should return proper value for all types of first arg', () => {
expect(toCurrency('100000')).toEqual('$100.000,00')
expect(toCurrency('1000.00')).toEqual('$1.000,00')
expect(toCurrency('1000,00')).toEqual('$1.000,00')
expect(toCurrency('100')).toEqual('$100,00')
expect(toCurrency(100000)).toEqual('$100.000,00')
expect(toCurrency(100000.01)).toEqual('$100.000,01')
expect(toCurrency(100000n)).toEqual('$100.000,00')
// expect(toCurrency(Number.MAX_SAFE_INTEGER)).toEqual('$100,000,000,000,000,000,000.00')
})
it('should return proper value for different currencies', () => {
expect(toCurrency('100000', '€')).toEqual('€100,000.00')
})
it('should return proper value for different precisions', () => {
expect(toCurrency('100000', '€', { int: 2, decimal: 2 })).toEqual('€100,000.000')
expect(toCurrency('100000', '€', { int: 0, decimal: 0 })).toEqual('€100,000')
})
it('should return proper value for different separators', () => {
expect(toCurrency('100000', '€', { int: 3, decimal: 4 }, { int: '/', decimal: '*', point: '.' })).toEqual(
'€100/000.0*000'
)
})
it('should throw an error if first arg has invalid type', () => {
expect(() => toCurrency({} as unknown as string)).toThrow(ERR_INVALID_TYPE)
})
it('should throw an error if first arg has invalid value', () => {
expect(() => toCurrency(Number.MAX_SAFE_INTEGER + 10)).toThrow(ERR_INTEGER_LIMIT)
expect(() => toCurrency('abc.abc')).toThrow(ERR_INVALID_STRING)
expect(() => toCurrency('abc,abc')).toThrow(ERR_INVALID_STRING)
expect(() => toCurrency('100.100.100')).toThrow(ERR_INVALID_STRING)
expect(() => toCurrency('100,100,100')).toThrow(ERR_INVALID_STRING)
})
it('should return proper value for negative values', () => {
expect(toCurrency('-100000')).toEqual('-$100,000.00')
})
it('should return proper value for fractional values', () => {
expect(toCurrency('100000.01')).toEqual('$100,000.01')
expect(toCurrency('100000,01')).toEqual('$100,000.01')
})
})