Skip to content

Instantly share code, notes, and snippets.

@stctheproducer
Created October 24, 2024 14:58
Show Gist options
  • Save stctheproducer/e8aa5fa2767af85dc112de8fc359f0ba to your computer and use it in GitHub Desktop.
Save stctheproducer/e8aa5fa2767af85dc112de8fc359f0ba to your computer and use it in GitHub Desktop.
Money class with mathjs evaluation and dinero.js
import * as currencies from '@dinero.js/currencies'
import {
allocate,
createDinero,
dinero,
type Dinero,
type Calculator,
type Currency,
type DivideOperation,
add,
subtract,
multiply,
greaterThanOrEqual,
greaterThan,
toDecimal,
lessThan,
lessThanOrEqual,
equal,
isNegative,
isZero,
isPositive,
toSnapshot,
toUnits,
convert,
minimum,
maximum,
haveSameAmount,
haveSameCurrency,
hasSubUnits,
normalizeScale,
transformScale,
trimScale,
type DineroSnapshot,
} from 'dinero.js'
import { evaluate } from 'mathjs'
const ComparisonOperator = {
LT: -1,
EQ: 0,
GT: 1,
} as const
const calculator: Calculator<bigint> = {
add: (augend, addend) => augend + addend,
compare: (a, b) => {
if (a < b) {
return ComparisonOperator.LT
}
if (a > b) {
return ComparisonOperator.GT
}
return ComparisonOperator.EQ
},
decrement: value => value - 1n,
increment: value => value + 1n,
integerDivide: (dividend, divisor) => dividend / divisor,
modulo: (dividend, divisor) => dividend % divisor,
multiply: (multiplicand, multiplier) => multiplicand * multiplier,
power: (base, exponent) => base ** exponent,
subtract: (minuend, subtrahend) => minuend - subtrahend,
zero: () => 0n,
}
type CurrencyCode = keyof typeof currencies
export class Money {
#value: Dinero<number>
#currency: Currency<number>
constructor(
num: number | Money | Dinero<number>,
currencyCode: string = 'ZMW',
isMinorCurrencyUnit: boolean = false,
) {
if (typeof num === 'number') {
let scale = Money.getScale(num),
currency = currencies[currencyCode as CurrencyCode] as Currency<number>
this.#currency = currency
scale += currency.exponent
const amount = isMinorCurrencyUnit
? num
: Money.convertToMinorCurrencyUnit(num, scale)
this.#value = dinero({
amount,
currency,
scale,
})
} else if (num instanceof Money) {
this.#currency = num.#currency
this.#value = num.#value
} else {
this.#currency = num.toJSON().currency
this.#value = num
}
}
static convertToMinorCurrencyUnit(num: number, scale: number) {
return Math.round(num * Math.pow(10, scale))
}
static isDecimal(num: number) {
const numString = num.toString()
return numString.includes('.')
}
static getScale(num: number): number {
// Convert the number to a string
const numString = num.toString()
// Check if there is a decimal point
if (numString.includes('.')) {
// Split the string by the decimal point and return the length of the decimal part
return numString.split('.')[1]!.length
} else {
// If there's no decimal point, return 0
return 0
}
}
static getScaledAmount(num: number) {
let scale = Money.getScale(num)
let amount = Money.convertToMinorCurrencyUnit(num, scale)
return { amount, scale }
}
#convertNumberToDinero(num: number, isMinorCurrencyUnit: boolean = false) {
let scale = Money.getScale(num),
amount = isMinorCurrencyUnit
? num
: Money.convertToMinorCurrencyUnit(num, scale)
return dinero({
amount,
currency: this.#currency,
scale,
})
}
#getDineroInstance(num: Money | number | Dinero<number>) {
let val: Dinero<number>
if (num instanceof Money) {
val = num.#value
} else if (typeof num === 'number') {
val = this.#convertNumberToDinero(num)
} else {
val = num
}
return val
}
distribute(ratios: number[]) {
return allocate(this.#value, ratios).map(d => new Money(d))
}
distributeEqually(numberOfShares: number) {
const ratios = Array.from({ length: numberOfShares }).fill(1) as number[]
return this.distribute(ratios)
}
distributeEquallyWithTax(
taxPercentage: number,
numberOfShares: number,
minimum: number | undefined = undefined,
) {
let percentage = Money.isDecimal(taxPercentage)
? Math.round(taxPercentage * 100)
: taxPercentage
const [tax, net] = this.distribute([percentage, 100])
const ratios = Array.from({ length: numberOfShares }).fill(1) as number[]
let allocated = net!
.distribute(ratios)
.map((n, i) => (i === 0 ? n.add(tax!) : n))
if (!minimum) {
return allocated
}
if (allocated[0]?.lessThan(minimum)) {
let first = new Money(minimum),
rest = new Money(this.#value)
.subtract(first)
.distributeEqually(numberOfShares - 1)
return [first, ...rest]
}
return allocated
}
distributeEquallyWithTaxAboveMinimum(
taxPercentage: number,
numberOfShares: number,
minimum: number,
) {
let percentage = Money.isDecimal(taxPercentage)
? Math.round(taxPercentage * 100)
: taxPercentage
const [tax, net] = this.distribute([percentage, 100])
if (!net?.lessThanOrEqual(minimum)) {
return this
}
const ratios = Array.from({ length: numberOfShares }).fill(1) as number[]
let allocated = net!
.distribute(ratios)
.map((n, i) => (i === 0 ? n.add(tax!) : n))
if (allocated[0]?.lessThan(minimum)) {
let first = new Money(minimum),
rest = new Money(this.#value)
.subtract(first)
.distributeEqually(numberOfShares - 1)
return [first, ...rest]
}
return allocated
}
distributeToDecimal(ratios: number[]) {
return allocate(this.#value, ratios).map(d => toDecimal(d))
}
distributeToNumber(ratios: number[]) {
return allocate(this.#value, ratios).map(d => Number(toDecimal(d)))
}
add(addend: Money | number | Dinero<number>) {
let val = this.#getDineroInstance(addend)
return new Money(add(this.#value, val))
}
addPercentage(num: number) {
let isDecimal = Money.isDecimal(num),
percent = isDecimal ? num : num / 100,
amt = multiply(this.#value, Money.getScaledAmount(percent))
return new Money(add(this.#value, amt))
}
subtract(subtrahend: Money | number | Dinero<number>) {
let val = this.#getDineroInstance(subtrahend)
return new Money(subtract(this.#value, val))
}
subtractPercentage(num: number) {
let isDecimal = Money.isDecimal(num),
percent = isDecimal ? num : num / 100,
amt = multiply(this.#value, Money.getScaledAmount(percent))
return new Money(subtract(this.#value, amt))
}
multiply(multiplicand: number) {
let isDecimal = Money.isDecimal(multiplicand)
const amt = isDecimal ? Money.getScaledAmount(multiplicand) : multiplicand
return new Money(multiply(this.#value, amt))
}
percentage(share: number, scale = 0) {
const power = scale + 1
const rest = 100 ** power - share
const [chunk] = allocate(this.#value, [
{ amount: share, scale },
{ amount: rest, scale },
])
return new Money(chunk!)
}
divide(divisor: number) {
let isDecimal = Money.isDecimal(divisor)
if (isDecimal) {
return this.multiply(1 / divisor)
}
const ratios = Array.from({ length: divisor }).fill(1) as number[]
const maxShare = maximum(this.distribute(ratios).map(m => m.#value))
return new Money(maxShare)
}
lessThan(num: Money | number | Dinero<number>) {
return lessThan(this.#value, this.#getDineroInstance(num))
}
lessThanOrEqual(num: Money | number | Dinero<number>) {
return lessThanOrEqual(this.#value, this.#getDineroInstance(num))
}
equal(num: Money | number | Dinero<number>) {
return equal(this.#value, this.#getDineroInstance(num))
}
greaterThanOrEqual(num: Money | number | Dinero<number>) {
return greaterThanOrEqual(this.#value, this.#getDineroInstance(num))
}
greaterThan(num: Money | number | Dinero<number>) {
return greaterThan(this.#value, this.#getDineroInstance(num))
}
isNegative() {
return isNegative(this.#value)
}
isZero() {
return isZero(this.#value)
}
isPositive() {
return isPositive(this.#value)
}
absolute() {
if (this.isNegative()) {
return new Money(multiply(this.#value, -1))
}
return this
}
minimum(...numbers: Array<number | Dinero<number> | Money>) {
let nums = numbers.map(n => this.#getDineroInstance(n))
let min = minimum([this.#value, ...nums])
return new Money(min)
}
maximum(...numbers: Array<number | Dinero<number> | Money>) {
let nums = numbers.map(n => this.#getDineroInstance(n))
let max = maximum([this.#value, ...nums])
return new Money(max)
}
haveSameAmount(...numbers: Array<number | Dinero<number> | Money>) {
let nums = numbers.map(n => this.#getDineroInstance(n))
return haveSameAmount([this.#value, ...nums])
}
haveSameCurrency(...numbers: Array<number | Dinero<number> | Money>) {
let nums = numbers.map(n => this.#getDineroInstance(n))
return haveSameCurrency([this.#value, ...nums])
}
hasSubUnits() {
return hasSubUnits(this.#value)
}
static fromSnapshot(snapshot: DineroSnapshot<number>) {
return new Money(dinero(snapshot))
}
toSnapshot() {
return toSnapshot(this.#value)
}
toJSON() {
return this.#value.toJSON()
}
get value() {
return Number(this.toDecimal())
}
get amount() {
return this.#value.toJSON().amount
}
toValue() {
return this.#value.toJSON().amount
}
toUnits() {
return toUnits(this.#value)
}
toDecimal() {
return toDecimal(this.#value)
}
toNumber() {
return Number(this.toDecimal())
}
format(locale = 'en-ZM', options: Intl.NumberFormatOptions = {}) {
return toDecimal(this.#value, ({ value, currency }) => {
return Number(value).toLocaleString(locale, {
...options,
style: 'currency',
currency: currency.code,
})
})
}
normalizeScale(...numbers: Array<number | Dinero<number> | Money>) {
let nums = numbers.map(n => this.#getDineroInstance(n))
return normalizeScale([this.#value, ...nums]).map(d => new Money(d))
}
transformScale(scale: number, divide?: DivideOperation) {
scale = Math.round(scale)
return new Money(transformScale(this.#value, scale, divide))
}
trimScale() {
return new Money(trimScale(this.#value))
}
convert(currencyCode: string, conversionRate: number) {
if (currencyCode === this.#currency.code) {
return this
}
const currency = currencies[
currencyCode as CurrencyCode
] as Currency<number>
const rates = {
[currencyCode]: Money.getScaledAmount(conversionRate),
}
return new Money(convert(this.#value, currency, rates))
}
static get dineroBigInt() {
return createDinero({ calculator })
}
static evaluateFormula(
formula: string,
variables: { [key: string]: number },
currencyCode = 'ZMW',
) {
let numericVariables: { [key: string]: number } = {},
hasError = false,
matches = formula.match(/\w+|\d+(\.\d+)?/g),
variablesList = Object.values(variables)
if (matches?.length !== variablesList.length) {
hasError = true
return new Money(0)
}
for (const [key, value] of Object.entries(variables)) {
if (typeof value !== 'number') {
hasError = true
break
}
numericVariables[key] = value
}
if (hasError) {
return new Money(0)
}
const amount = evaluate(formula, numericVariables)
return new Money(amount, currencyCode)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment