Skip to content

Instantly share code, notes, and snippets.

@stctheproducer
Last active September 2, 2025 14:36
Show Gist options
  • Select an option

  • Save stctheproducer/3ca2c0622896585ba1abe08a3747164e to your computer and use it in GitHub Desktop.

Select an option

Save stctheproducer/3ca2c0622896585ba1abe08a3747164e to your computer and use it in GitHub Desktop.
This TypeScript file provides a money library for handling monetary values with precision. It uses `BigInt` to accurately store amounts in their minor currency units, avoiding floating-point issues. Leveraging `Intl.NumberFormat`, it offers robust and localized formatting of monetary values.
/**
* Represents a monetary value using BigInt for precision.
*/
export interface Currency<T = number> {
code: string
base: T | readonly T[]
exponent: number // Number of decimal places in the minor unit
}
// A simple registry of supported currencies
const currencies: { [key: string]: Currency<number> } = {
ZMW: { code: 'ZMW', base: 10, exponent: 2 }, // Zambian Kwacha
USD: { code: 'USD', base: 10, exponent: 2 }, // US Dollar
EUR: { code: 'EUR', base: 10, exponent: 2 }, // Euro
GBP: { code: 'GBP', base: 10, exponent: 2 }, // British Pound
ZAR: { code: 'ZAR', base: 10, exponent: 2 }, // South African Rand
// Add other currencies as needed or get them from the `@dinero.js/currencies@alpha` NPM package
}
/**
* Converts a major unit number/string to a BigInt minor unit based on a target scale.
* It handles padding with zeros if the input has fewer decimal places than targetScale,
* and performs "round half up" rounding if the input has more decimal places.
*
* @param amount Major unit amount (e.g., 12.34 or '1.23456')
* @param targetScale The desired number of decimal places for the minor unit.
* @returns Amount in minor units as BigInt, correctly rounded.
*/
function majorToMinor(amount: number | string, targetScale: number): bigint {
const amountStr = String(amount)
const sign = amountStr.startsWith('-') ? -1n : 1n
const absoluteAmountStr = amountStr.replace('-', '') // Work with absolute value for rounding
const [integerPart, decimalPart] = absoluteAmountStr.split('.')
const currentDecimalPlaces = decimalPart ? decimalPart.length : 0
// Convert the integer and decimal parts into a single BigInt without a decimal point.
// This BigInt initially represents the number scaled by its *original* decimal places.
const rawBigIntFromAbsolute = BigInt(`${integerPart}${decimalPart || ''}`)
if (currentDecimalPlaces <= targetScale) {
// If the input has fewer or equal decimal places than targetScale,
// just pad with zeros to reach the targetScale. No rounding needed here.
const scaleDifference = targetScale - currentDecimalPlaces
return rawBigIntFromAbsolute * BigInt(10) ** BigInt(scaleDifference) * sign
} else {
// Input has more decimal places than targetScale. Need to perform rounding.
const excessDecimalPlaces = currentDecimalPlaces - targetScale
const powerOfTenExcess = BigInt(10) ** BigInt(excessDecimalPlaces)
// Round half up: add half of the divisor before performing integer division.
// For example, to round to 2 decimal places, if we have a number scaled by 3 decimal places (e.g., 1235n for 1.235),
// we need to divide by 10. Adding 5 (half of 10) before division will round correctly.
const roundingThreshold = powerOfTenExcess / 2n
// Apply rounding before scaling down to the targetScale.
// The addition of `roundingThreshold` effectively implements "round half up"
// (rounding away from zero for values ending in .5).
const roundedBigInt = (rawBigIntFromAbsolute + roundingThreshold) / powerOfTenExcess
return roundedBigInt * sign // Apply the original sign
}
}
/**
* Creates a number formatter based on currency and locale.
* @param currency The currency object.
* @param locale The locale string (e.g., 'en-ZM').
* @param options Intl.NumberFormatOptions.
* @returns Intl.NumberFormat instance.
*/
function getFormatter(
currency: Currency,
locale: string,
options: Intl.NumberFormatOptions
): Intl.NumberFormat {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency.code,
minimumFractionDigits: currency.exponent,
maximumFractionDigits: currency.exponent,
...options,
})
}
export class Money {
#amount: bigint // Amount in the smallest currency unit (e.g., cents)
#currency: Currency
#scale: number
/**
* Creates a new Money instance.
* @param amount The amount in major units (e.g., 12.34) or a Money instance, or a BigInt minor unit as string.
* @param currencyCode The currency code (default: 'ZMW').
* @param isMinorUnit If true, the amount is treated as a minor unit BigInt string.
* @param explicitScale Optional: When isMinorUnit is true, this explicitly sets the scale.
* Otherwise, the currency's exponent is used as the minor unit scale.
*/
constructor(
amount: number | string | Money,
currencyCode: string | undefined = undefined,
isMinorUnit: boolean = false,
explicitScale: number | undefined = undefined
) {
let finalCurrencyCode: string
// Determine the final currency code
if (amount instanceof Money) {
// If 'amount' is already a Money instance
if (currencyCode === undefined) {
// If no currencyCode is provided, infer it from the Money instance
finalCurrencyCode = amount.currency.code
} else if (currencyCode !== amount.currency.code) {
// If a currencyCode is provided and it's different from the Money instance's currency,
// this is generally an error (or implies a conversion, which should be explicit).
// For simplicity, we'll throw an error unless explicit conversion is intended.
throw new Error(
`Cannot create Money from instance with different currency (${amount.currency.code}) than target (${currencyCode}) without explicit conversion.`
)
} else {
finalCurrencyCode = currencyCode
}
} else {
if (currencyCode === undefined) {
// If 'amount' is a number or string, currencyCode defaults to 'ZMW'
finalCurrencyCode = 'ZMW'
} else {
finalCurrencyCode = currencyCode
}
}
// Validate and set the currency
const currency = currencies[finalCurrencyCode]
if (!currency) {
throw new Error(`Unsupported currency code: ${finalCurrencyCode}`)
}
this.#currency = currency
if (amount instanceof Money) {
if (amount.currency.code !== this.#currency.code) {
throw new Error(
`Cannot create Money from instance with different currency (${amount.currency.code}) than target (${this.#currency.code}) without explicit conversion.`
)
}
this.#amount = amount.amount
this.#scale = amount.scale // Preserve the source Money instance's scale
} else {
if (isMinorUnit) {
// Treat the number/string directly as minor units (BigInt)
this.#amount = BigInt(amount)
// If an explicit scale is provided, use it, otherwise fall back to currency's exponent
this.#scale = explicitScale !== undefined ? explicitScale : this.#currency.exponent
} else {
// Convert major unit number/string to minor unit BigInt
const amountStr = String(amount)
const [, decimalPart] = amountStr.split('.')
// Determine the scale: use input's decimal places, but ensure it's at least the currency's exponent
this.#scale = decimalPart
? Math.max(decimalPart.length, this.#currency.exponent)
: this.#currency.exponent
this.#amount = majorToMinor(amount, this.#scale)
}
}
}
/**
* Gets the scale of this Money instance.
*/
get scale(): number {
return this.#scale
}
/**
* Gets the amount in the smallest currency unit as BigInt.
*/
get amount(): bigint {
return this.#amount
}
/**
* Returns the internal amount (#amount) adjusted (scaled) to the currency's exponent.
* If the instance's scale is higher than the currency's exponent (scaling down),
* the amount will be rounded using "round half up". If scaling up, it's padded with zeros.
* @returns The amount as a BigInt, scaled to the currency's exponent.
*/
get scaledAmount(): bigint {
const currentExponent = BigInt(this.#scale)
const currencyExponent = BigInt(this.#currency.exponent)
if (currentExponent === currencyExponent) {
return this.#amount
}
const exponentDifference = currentExponent - currencyExponent
if (exponentDifference > 0n) {
// Current amount has more decimal places (higher precision) than currency's exponent.
// Need to scale down by dividing. Apply "round half up".
const divisor = BigInt(10) ** exponentDifference
const roundingThreshold = divisor / 2n // For "round half up"
// Add half of the divisor before performing integer division.
return (this.#amount + roundingThreshold) / divisor
} else {
// Current amount has fewer decimal places (lower precision) than currency's exponent.
// Need to scale up by multiplying (padding with zeros).
const factor = BigInt(10) ** -exponentDifference // -exponentDifference will be positive
return this.#amount * factor
}
}
/**
* Gets the currency of this Money instance.
*/
get currency(): Currency {
return this.#currency
}
/**
* Creates a Money instance from a BigInt minor unit amount and an explicit scale.
* Useful for constructing results of arithmetic operations.
* @param amount The amount in minor units as BigInt.
* @param currencyCode The currency code.
* @param scale The explicit scale (number of decimal places) for this amount.
* @returns A new Money instance.
*/
static fromMinorAndScale(amount: bigint, currencyCode: string, scale: number): Money {
// Pass true for isMinorUnit and the explicit scale
return new Money(String(amount), currencyCode, true, scale)
}
/**
* Normalizes two Money instances to their highest common scale for arithmetic operations.
* Throws an error if currencies differ.
* @param moneyA The first Money instance.
* @param moneyB The second Money instance.
* @returns An object containing the normalized BigInt amounts and the common scale.
*/
private static normalizeAmounts(
moneyA: Money,
moneyB: Money
): { amountA: bigint; amountB: bigint; commonScale: number } {
if (moneyA.currency.code !== moneyB.currency.code) {
throw new Error(
`Cannot perform operations on Money instances with different currencies (${moneyA.currency.code} and ${moneyB.currency.code}).`
)
}
const commonScale = Math.max(moneyA.scale, moneyB.scale)
// Calculate scaling factors
const scaleFactorA = BigInt(10) ** BigInt(commonScale - moneyA.scale)
const scaleFactorB = BigInt(10) ** BigInt(commonScale - moneyB.scale)
// Apply scaling
const amountA = moneyA.amount * scaleFactorA
const amountB = moneyB.amount * scaleFactorB
return { amountA, amountB, commonScale }
}
// Helper to convert non-Money operand to Money instance for internal use
private toMoneyOperand(operand: number | string): Money {
// When converting a number/string operand, use the current instance's currency
// The constructor will derive the correct scale for this temporary Money instance.
return new Money(operand, this.#currency.code)
}
/**
* Private helper to rescale a BigInt amount from one implied scale to another.
* Applies "round half up" if scaling down.
* @param amount The BigInt amount to rescale.
* @param currentImpliedScale The scale this BigInt 'currently' represents.
* @param targetScale The desired scale for the BigInt.
* @returns The rescaled BigInt amount.
*/
private static rescaleBigInt(
amount: bigint,
currentImpliedScale: number,
targetScale: number
): bigint {
const currentBig = BigInt(currentImpliedScale)
const targetBig = BigInt(targetScale)
if (currentBig === targetBig) {
return amount
}
const exponentDifference = targetBig - currentBig // target - current
if (exponentDifference > 0n) {
// Scaling UP (e.g., from scale 2 to 3 means minor unit becomes smaller)
// Amount needs to be multiplied.
const factor = BigInt(10) ** exponentDifference
return amount * factor
} else {
// Scaling DOWN (e.g., from scale 3 to 2 means minor unit becomes larger)
// Amount needs to be divided, with rounding.
const divisor = BigInt(10) ** -exponentDifference // This is positive
const roundingThreshold = divisor / 2n
// Apply "round half up"
return (amount + roundingThreshold) / divisor
}
}
/**
* Adds another Money instance or a number to this Money instance.
* Ensures scale is taken into consideration for precision.
* @param addend The amount to add.
* @returns A new Money instance with the result.
*/
add(addend: Money | number | string): Money {
const otherMoney = addend instanceof Money ? addend : this.toMoneyOperand(addend)
const { amountA, amountB, commonScale } = Money.normalizeAmounts(this, otherMoney)
const resultAmount = amountA + amountB
return Money.fromMinorAndScale(resultAmount, this.#currency.code, commonScale)
}
/**
* Subtracts another Money instance or a number from this Money instance.
* Ensures scale is taken into consideration for precision.
* @param subtrahend The amount to subtract.
* @returns A new Money instance with the result.
*/
subtract(subtrahend: Money | number | string): Money {
const otherMoney = subtrahend instanceof Money ? subtrahend : this.toMoneyOperand(subtrahend)
const { amountA, amountB, commonScale } = Money.normalizeAmounts(this, otherMoney)
const resultAmount = amountA - amountB
return Money.fromMinorAndScale(resultAmount, this.#currency.code, commonScale)
}
/**
* Multiplies this Money instance by a number.
* The resulting Money instance's scale can be optionally defined by `targetScale`.
* If `targetScale` is not provided, the scale will be the sum of this Money's scale
* and the multiplier's decimal places.
* @param multiplier The number to multiply by.
* @param targetScale Optional: The desired scale of the resulting Money instance.
* @returns A new Money instance with the result.
*/
multiply(multiplier: number | string, targetScale: number | undefined = undefined): Money {
const multiplierStr = String(multiplier)
const [, decimalPart] = multiplierStr.split('.')
const multiplierDecimalPlaces = decimalPart ? decimalPart.length : 0 // Scale of the multiplier number
const multiplierBigInt = majorToMinor(multiplierStr, multiplierDecimalPlaces) // Convert multiplier to a BigInt based on its own precision
// The result of `this.#amount * multiplierBigInt` has an implied scale
// that is the sum of `this.#scale` and `multiplierDecimalPlaces`.
const intermediateProduct = this.#amount * multiplierBigInt
const impliedResultScale = this.#scale + multiplierDecimalPlaces
let finalAmount: bigint
let finalScale: number
if (targetScale !== undefined) {
// If a targetScale is provided, rescale the intermediate product to that target.
finalScale = targetScale
finalAmount = Money.rescaleBigInt(intermediateProduct, impliedResultScale, finalScale)
} else {
// Otherwise, keep the full implied precision.
finalScale = impliedResultScale
finalAmount = intermediateProduct
}
return Money.fromMinorAndScale(finalAmount, this.#currency.code, finalScale)
}
// Inside Money class
/**
* Divides this Money instance by a number.
* The resulting Money instance's scale can be optionally defined by `targetScale`.
* If `targetScale` is not provided, a fixed amount of extra precision is added to reduce truncation.
* @param divisor The number to divide by. Must not be zero.
* @param targetScale Optional: The desired scale of the resulting Money instance.
* @returns A new Money instance with the result.
* @throws Error if divisor is zero.
*/
divide(divisor: number | string, targetScale: number | undefined = undefined): Money {
const divisorMoney = new Money(divisor, this.#currency.code) // Convert divisor to a Money instance to properly get its scale
const divisorAmount = divisorMoney.amount
const divisorScale = divisorMoney.scale
if (divisorAmount === 0n) {
throw new Error('Division by zero.')
}
// Determine a high-precision scale for the preliminary division result,
// before applying any explicit targetScale.
const DIVISION_PRECISION_BUFFER = 6
const unconstrainedResultScale =
Math.max(this.#scale, this.#currency.exponent) + DIVISION_PRECISION_BUFFER
// Perform the division at high precision.
const numerator = this.#amount * BigInt(10) ** BigInt(divisorScale + unconstrainedResultScale)
const denominator = divisorAmount * BigInt(10) ** BigInt(this.#scale)
const preliminaryResultAmount = numerator / denominator
let finalAmount: bigint
let finalScale: number
if (targetScale !== undefined) {
// If a targetScale is provided, rescale the preliminary result to that target.
finalScale = targetScale
finalAmount = Money.rescaleBigInt(
preliminaryResultAmount,
unconstrainedResultScale,
finalScale
)
} else {
// Otherwise, keep the high unconstrained precision.
finalScale = unconstrainedResultScale
finalAmount = preliminaryResultAmount
}
return Money.fromMinorAndScale(finalAmount, this.#currency.code, finalScale)
}
/**
* Allocates the money into multiple shares based on ratios.
* Returns shares at the instance's current scale by default, or at a specified `returnScale`.
* @param ratios An array of numbers representing the ratios for allocation.
* @param returnScale Optional: The desired scale for the returned Money instances. Defaults to the instance's current scale.
* @returns An array of new Money instances representing the allocated shares.
*/
allocate(ratios: number[], returnScale: number | undefined = undefined): Money[] {
if (ratios.length === 0) {
return []
}
const totalRatio = ratios.reduce((sum, ratio) => sum + ratio, 0)
if (totalRatio <= 0) {
throw new Error('Total ratio must be positive for allocation.')
}
let allocatedAmounts: bigint[] = []
let totalAllocated = 0n
const RATIO_SCALING_FACTOR = BigInt(10) ** BigInt(6)
const totalRatioScaled = BigInt(Math.round(totalRatio * Number(RATIO_SCALING_FACTOR)))
for (const loopRatio of ratios) {
const ratioScaled = BigInt(Math.round(loopRatio * Number(RATIO_SCALING_FACTOR)))
// Shares are calculated at the current instance's scale (this.#scale).
const share = (this.#amount * ratioScaled) / totalRatioScaled
allocatedAmounts.push(share)
totalAllocated += share
}
// Handle potential remainder.
const remainder = this.#amount - totalAllocated
for (let i = 0; i < remainder; i++) {
if (i < allocatedAmounts.length) {
allocatedAmounts[i]++
} else {
console.warn(
'Allocation remainder exceeds number of shares. This indicates an issue with remainder calculation or share count.'
)
}
}
// Determine the final scale for the returned Money instances.
const finalReturnScale = returnScale !== undefined ? returnScale : this.#scale
// Return new Money instances, rescaling each allocated amount to the `finalReturnScale`.
return allocatedAmounts.map((amount) =>
Money.fromMinorAndScale(
Money.rescaleBigInt(amount, this.#scale, finalReturnScale), // Rescale amount from original scale to finalReturnScale
this.#currency.code,
finalReturnScale
)
)
}
/**
* Distributes the money equally into a specified number of shares.
* Returns shares at the currency's natural exponent scale.
* @param numberOfShares The number of equal shares to distribute into.
* @returns An array of new Money instances representing the equal shares.
* @throws Error if numberOfShares is less than or equal to zero.
*/
distributeEqually(numberOfShares: number): Money[] {
if (numberOfShares <= 0) {
throw new Error('Number of shares must be positive for equal distribution.')
}
const ratios = Array(numberOfShares).fill(1)
// Call allocate, and explicitly ask for the shares to be at the currency's natural exponent.
return this.allocate(ratios, this.#currency.exponent)
}
/**
* Formats the money amount using Intl.NumberFormat.
* @param locale The locale string (default: 'en-ZM').
* @param options Optional Intl.NumberFormatOptions.
* @returns The formatted string.
*/
format(locale: string = 'en-ZM', options: Intl.NumberFormatOptions = {}): string {
const formatter = getFormatter(this.#currency, locale, options)
// Convert the BigInt minor unit amount to a major unit number for formatting.
// This might involve precision loss for display, which is acceptable for formatting.
return formatter.format(Number(this.toDecimal()))
}
/**
* Returns the amount in major units as a number.
* Note: This conversion can lead to floating-point inaccuracies for display purposes.
* Use `format` or `toDecimal` for precise string representation.
* @returns The amount in major units as a number.
*/
toNumber(): number {
return Number(this.toDecimal()) // Safer to convert `toDecimal()` string to number
}
/**
* Returns the amount in major units as a string.
* This avoids potential floating-point issues of `toNumber()` and respects the instance's scale.
* @returns The amount in major units as a string.
*/
toDecimal(): string {
const divisor = BigInt(10) ** BigInt(this.#scale) // Use this.#scale here
const majorPart = this.#amount / divisor
let minorPart = this.#amount % divisor
// Handle negative numbers for the minor part. If total amount is negative,
// the remainder might also be negative, but we want the absolute minor part for display.
if (minorPart < 0n) {
minorPart = -minorPart
}
// Pad the minor part with leading zeros to match the current instance's scale
let minorString = minorPart.toString().padStart(this.#scale, '0')
// If the minor part is '0', padStart will return '0' for exponent 0, or '00' for exponent 2 etc.
// But we might have a minor part that is, e.g., '5' for scale 2, should be '50'.
// `padStart(this.#scale, '0')` should work correctly for this.
// Combine integer and decimal parts
let resultString = `${majorPart}.${minorString}`
// If the amount is negative and the major part is 0 (e.g., -0.5), ensure the negative sign is there.
if (this.#amount < 0n && majorPart === 0n && minorPart !== 0n) {
resultString = `-${resultString.replace('-', '')}`
}
// If scale is 0, no decimal point is needed
if (this.#scale === 0) {
return this.#amount.toString()
}
return resultString
}
// Helper to safely convert 'other' operand to a Money instance for comparison
private getOtherMoneyForComparison(other: Money | number | string): Money {
if (other instanceof Money) {
if (other.currency.code !== this.#currency.code) {
throw new Error(
`Cannot compare Money instances with different currencies (${this.#currency.code} and ${other.currency.code}).`
)
}
return other
} else {
// Create a temporary Money instance for the number/string operand.
// The constructor will derive its scale correctly.
return new Money(other, this.#currency.code)
}
}
/**
* Helper to safely convert 'other' operand to a Money instance for operations
* where currency check is explicitly ignored (e.g., haveSameAmount).
* It creates the Money instance using the current instance's currency code.
* @param other The other operand (Money, number, or string).
* @returns A Money instance representing the other operand.
*/
private getOtherMoneyNoCurrencyCheck(other: Money | number | string): Money {
if (other instanceof Money) {
return other // Already a Money instance
} else {
// For number/string, create a Money instance using this instance's currency.
// The constructor will derive the appropriate scale for the new Money instance.
return new Money(other, this.#currency.code)
}
}
/**
* Checks if this Money instance is less than another.
* Compares values after normalizing to a common scale.
* @param other The other Money instance or number to compare with.
* @returns True if this instance is less than the other.
*/
lessThan(other: Money | number | string): boolean {
const otherMoney = this.getOtherMoneyForComparison(other)
const { amountA, amountB } = Money.normalizeAmounts(this, otherMoney)
return amountA < amountB
}
/**
* Checks if this Money instance is less than or equal to another.
* Compares values after normalizing to a common scale.
* @param other The other Money instance or number to compare with.
* @returns True if this instance is less than or equal to the other.
*/
lessThanOrEqual(other: Money | number | string): boolean {
const otherMoney = this.getOtherMoneyForComparison(other)
const { amountA, amountB } = Money.normalizeAmounts(this, otherMoney)
return amountA <= amountB
}
/**
* Checks if this Money instance is equal to another.
* Currency must also be the same, and values are compared after normalizing to a common scale.
* @param other The other Money instance or number to compare with.
* @returns True if this instance is equal to the other.
*/
equal(other: Money | number | string): boolean {
if (other instanceof Money) {
if (other.currency.code !== this.#currency.code) {
// If the other operand is a Money instance and currencies differ, they can't be equal.
return false
}
}
const otherMoney = this.getOtherMoneyForComparison(other) // Will throw if currency differs for non-Money
const { amountA, amountB } = Money.normalizeAmounts(this, otherMoney)
return amountA === amountB
}
/**
* Checks if this Money instance is greater than another.
* Compares values after normalizing to a common scale.
* @param other The other Money instance or number to compare with.
* @returns True if this instance is greater than the other.
*/
greaterThan(other: Money | number | string): boolean {
const otherMoney = this.getOtherMoneyForComparison(other)
const { amountA, amountB } = Money.normalizeAmounts(this, otherMoney)
return amountA > amountB
}
/**
* Checks if this Money instance is greater than or equal to another.
* Compares values after normalizing to a common scale.
* @param other The other Money instance or number to compare with.
* @returns True if this instance is greater than or equal to the other.
*/
greaterThanOrEqual(other: Money | number | string): boolean {
const otherMoney = this.getOtherMoneyForComparison(other)
const { amountA, amountB } = Money.normalizeAmounts(this, otherMoney)
return amountA >= amountB
}
/**
* Checks if the amount is negative.
* @returns True if the amount is less than zero.
*/
isNegative(): boolean {
return this.#amount < 0n
}
/**
* Checks if the amount is zero.
* @returns True if the amount is equal to zero.
*/
isZero(): boolean {
return this.#amount === 0n
}
/**
* Checks if the amount is positive.
* @returns True if the amount is greater than zero.
*/
isPositive(): boolean {
return this.#amount > 0n
}
/**
* Returns the absolute value of the money amount.
* @returns A new Money instance with the absolute value, maintaining the original scale.
*/
absolute(): Money {
const absAmount = this.#amount < 0n ? -this.#amount : this.#amount
return Money.fromMinorAndScale(absAmount, this.#currency.code, this.#scale)
}
/**
* Finds the minimum among this Money instance and others.
* All instances must have the same currency. Compares values after normalizing to a common scale.
* @param others Other Money instances or numbers to compare.
* @returns The minimum Money instance (the actual object that was found to be the minimum).
* @throws Error if currencies differ.
*/
minimum(...others: Array<Money | number | string>): Money {
let minMoney = Money.fromMinorAndScale(this.#amount, this.#currency.code, this.#scale) // Start with this instance as the current minimum candidate
for (const other of others) {
const otherMoney = this.getOtherMoneyForComparison(other) // Safely get other as Money instance
// Normalize current minimum candidate and the other Money instance for comparison
const { amountA, amountB } = Money.normalizeAmounts(minMoney, otherMoney)
if (amountB < amountA) {
minMoney = otherMoney // If other is smaller, it becomes the new minimum candidate
}
}
// Return the Money instance that was determined to be the minimum.
// It retains its original scale.
return minMoney
}
/**
* Finds the maximum among this Money instance and others.
* All instances must have the same currency. Compares values after normalizing to a common scale.
* @param others Other Money instances or numbers to compare.
* @returns The maximum Money instance (the actual object that was found to be the maximum).
* @throws Error if currencies differ.
*/
maximum(...others: Array<Money | number | string>): Money {
let maxMoney = Money.fromMinorAndScale(this.#amount, this.#currency.code, this.#scale) // Start with this instance as the current maximum candidate
for (const other of others) {
const otherMoney = this.getOtherMoneyForComparison(other) // Safely get other as Money instance
// Normalize current maximum candidate and the other Money instance for comparison
const { amountA, amountB } = Money.normalizeAmounts(maxMoney, otherMoney)
if (amountB > amountA) {
maxMoney = otherMoney // If other is larger, it becomes the new maximum candidate
}
}
// Return the Money instance that was determined to be the maximum.
// It retains its original scale.
return maxMoney
}
/**
* Checks if this Money instance and others have the same numerical amount.
* Currency is ignored for this check. Values are compared after normalizing to a common scale.
* @param others Other Money instances or numbers to compare.
* @returns True if all instances have the same numerical amount.
*/
haveSameAmount(...others: Array<Money | number | string>): boolean {
// Convert this instance's amount to BigInt at its current scale
const thisAmount = this.#amount
const thisScale = this.#scale
for (const other of others) {
const otherMoney = this.getOtherMoneyNoCurrencyCheck(other) // Get Money instance, no currency check
// Normalize amounts to the highest common scale for comparison
// The normalizeAmounts function expects both operands to be Money instances
// and internally handles the scaling of their BigInt amounts.
// Although normalizeAmounts has a currency check, we are passing in Money
// instances that either have the same currency (if from `this`), or
// a temporary Money instance created with `this.#currency.code`.
// So the check passes for a *technical* match of currency code in the Money object,
// allowing the value comparison to proceed regardless of actual real-world currency.
const { amountA, amountB } = Money.normalizeAmounts(
Money.fromMinorAndScale(thisAmount, this.#currency.code, thisScale), // Create temp Money from `this`
otherMoney
)
if (amountA !== amountB) {
return false
}
}
return true
}
/**
* Checks if this Money instance and others have the same currency.
* @param others Other Money instances to compare.
* @returns True if all instances have the same currency code.
*/
haveSameCurrency(...others: Array<Money>): boolean {
for (const other of others) {
if (this.#currency.code !== other.currency.code) {
return false
}
}
return true
}
/**
* Checks if the amount has subunits (i.e., a non-zero minor unit component).
* @returns True if the amount has a non-zero remainder when divided by 10^exponent.
*/
hasSubUnits(): boolean {
const divisor = BigInt(10) ** BigInt(this.#currency.exponent)
return this.#amount % divisor !== 0n
}
/**
* Converts this Money instance to a different currency, maintaining precision and applying rounding.
*
* @param targetCurrencyCode The currency code to convert to.
* @param conversionRate The rate to multiply by (e.g., 1 USD = 0.85 EUR, rate is 0.85 when converting USD to EUR).
* @returns A new Money instance in the target currency.
* @throws Error if the target currency is not supported or conversion rate is invalid.
*/
convert(targetCurrencyCode: string, conversionRate: number | string): Money {
const targetCurrency = currencies[targetCurrencyCode]
if (!targetCurrency) {
throw new Error(`Unsupported target currency code for conversion: ${targetCurrencyCode}`)
}
const rateNum = Number(conversionRate)
if (Number.isNaN(rateNum) || rateNum <= 0) {
throw new Error('Invalid conversion rate: Must be a positive number.')
}
if (this.#currency.code === targetCurrency.code) {
// If converting to the same currency, create a new instance with the target currency's exponent
// to normalize its scale, but maintain the original value.
// This ensures the resulting Money object reflects the target currency's official precision.
// We explicitly convert to a BigInt at the target currency's exponent.
const convertedAmountAtTargetExponent = majorToMinor(
this.toDecimal(),
targetCurrency.exponent
)
return Money.fromMinorAndScale(
convertedAmountAtTargetExponent,
targetCurrency.code,
targetCurrency.exponent
)
}
// Determine the scale of the conversion rate itself.
const conversionRateStr = String(conversionRate)
const [, rateDecimalPart] = conversionRateStr.split('.')
const rateDecimalPlaces = rateDecimalPart ? rateDecimalPart.length : 0
// Convert the conversion rate to a BigInt based on its decimal places.
const rateBigInt = BigInt(
`${rateDecimalPart ? conversionRateStr.replace('.', '') : conversionRateStr}`
)
// Multiply the current Money's internal amount by the rate BigInt.
// The current amount (#amount) is at `this.#scale` precision.
// The `rateBigInt` implicitly represents `rateNum * (10 ** rateDecimalPlaces)`.
// So, `this.#amount * rateBigInt` will be scaled by `this.#scale + rateDecimalPlaces`.
const intermediateProduct = this.#amount * rateBigInt
const intermediateProductScale = this.#scale + rateDecimalPlaces
// Calculate the required scaling factor to bring the `intermediateProduct`
// from its `intermediateProductScale` down to the `targetCurrency.exponent`.
const scaleDifference = intermediateProductScale - targetCurrency.exponent
let finalConvertedAmount: bigint
if (scaleDifference > 0) {
// We need to scale down (divide) the `intermediateProduct`.
// Apply "round half up" before division to truncate excess precision.
const powerOfTenToDivideBy = BigInt(10) ** BigInt(scaleDifference)
const roundingThreshold = powerOfTenToDivideBy / 2n // For "round half up"
finalConvertedAmount = (intermediateProduct + roundingThreshold) / powerOfTenToDivideBy
} else if (scaleDifference < 0) {
// We need to scale up (multiply) the `intermediateProduct`.
const powerOfTenToMultiplyBy = BigInt(10) ** BigInt(Math.abs(scaleDifference))
finalConvertedAmount = intermediateProduct * powerOfTenToMultiplyBy
} else {
// No scaling difference, use the product as is.
finalConvertedAmount = intermediateProduct
}
// Create a new Money instance using the target currency and its natural exponent as the scale.
// The finalConvertedAmount is already scaled to the target currency's exponent.
return Money.fromMinorAndScale(
finalConvertedAmount,
targetCurrency.code,
targetCurrency.exponent
)
}
// Note: Methods like normalizeScale, transformScale, trimScale from Dinero.js
// relate to managing the internal scale of the Dinero object.
// With our BigInt approach representing a fixed minor unit based on currency exponent,
// these concepts might map differently or be less necessary.
// The `toDecimal()` and `toNumber()` methods handle conversion to major units for display.
// If scale manipulation on the internal BigInt is needed, it would involve
// multiplying or dividing the BigInt by powers of 10 and potentially updating the currency exponent,
// which changes the meaning of the BigInt amount.
// Implementing transformScale as an example:
/**
* Transforms the internal scale of the money amount.
* This changes the unit that the internal BigInt represents. Use with caution.
* When scaling down (newExponent < currentExponent), "round half up" is applied.
* @param newExponent The new currency exponent (scale) to transform to.
* @returns A new Money instance with the transformed scale.
* @throws Error if newExponent is negative.
*/
transformScale(newExponent: number): Money {
if (newExponent < 0) {
throw new Error('New exponent cannot be negative.')
}
const currentExponent = BigInt(this.#scale)
const targetExponent = BigInt(newExponent)
if (targetExponent === currentExponent) {
// No change in scale, return a clone for immutability, preserving the current scale.
return Money.fromMinorAndScale(this.#amount, this.#currency.code, this.#scale)
}
const exponentDifference = targetExponent - currentExponent
let transformedAmount: bigint
if (exponentDifference > 0n) {
// Scaling up the exponent means the minor unit becomes smaller.
// The BigInt amount needs to be multiplied to represent the same value in smaller units.
// E.g., 1.23 (scale 2, amount 123n) to scale 3 (1.230): amount 123n * 10^1 = 1230n
const factor = BigInt(10) ** exponentDifference
transformedAmount = this.#amount * factor
} else {
// Scaling down the exponent means the minor unit becomes larger.
// The BigInt amount needs to be divided to represent the same value in larger units.
// E.g., 1.234 (scale 3, amount 1234n) to scale 2 (1.23): amount 1234n / 10^1
// We need to apply "round half up" here.
const divisor = BigInt(10) ** -exponentDifference // -exponentDifference will be positive
// Apply "round half up": add half of the divisor before integer division.
const roundingThreshold = divisor / 2n
transformedAmount = (this.#amount + roundingThreshold) / divisor
}
// Create a new Money instance with the transformed amount and the new currency representation.
// The amount is already in the units of the new exponent, and the new instance's scale
// should reflect this new exponent.
return Money.fromMinorAndScale(transformedAmount, this.#currency.code, newExponent)
}
// JSON serialization
toJSON(): { amount: string; currency: Currency; scale: number } {
return {
amount: this.#amount.toString(), // Store BigInt as string
currency: this.#currency,
scale: this.#scale, // Store the instance's scale for faithful deserialization
}
}
static fromJSON(json: { amount: string; currency: Currency; scale: number }): Money {
// Use fromMinorAndScale to reconstruct with the correct BigInt amount and explicit scale
return Money.fromMinorAndScale(BigInt(json.amount), json.currency.code, json.scale)
}
// Example of a static helper to create Money from major unit number
static fromMajor(
amount: number | string,
currencyCode: string = 'ZMW',
scale: number | undefined = undefined
): Money {
return new Money(amount, currencyCode, false, scale)
}
// Example of a static helper to create Money from minor unit BigInt
static fromMinor(
amount: bigint,
currencyCode: string = 'ZMW',
scale: number | undefined = undefined
): Money {
return new Money(String(amount), currencyCode, true, scale)
}
// --- Formula Evaluation ---
// Adapting the evaluateFormula from the original file.
// This part still uses `mathjs` and might require careful handling if full BigInt precision
// is needed within the formula evaluation itself. `mathjs` primarily works with numbers or its own BigNumber type.
// For this implementation, I'll keep the structure but note that mathjs might lose precision
// if the intermediate results exceed standard number limits or require decimal precision that mathjs handles with floats.
// A truly BigInt-based formula evaluation would require a different math expression parser.
/**
* Evaluates a mathematical formula with given variables.
* Note: Uses `mathjs` which may not provide full BigInt precision for complex expressions.
* Intermediate calculations in mathjs are typically done with numbers or its own BigNumber type.
* The final result is converted back to Money.
* @param formula The mathematical formula string (e.g., "a + b * c").
* @param variables An object mapping variable names to their numeric values.
* @param currencyCode The currency code for the result (default: 'ZMW').
* @param debug Optional debug flag.
* @returns A new Money instance representing the result of the formula.
* @throws Error if formula evaluation fails.
*/
// static evaluateFormula(
// formula: string,
// variables: { [key: string]: number },
// currencyCode: string = 'ZMW',
// debug: boolean = false // Retain debug flag from original
// ): Money {
// if (!formula) {
// return new Money(0, currencyCode)
// }
// try {
// // Use mathjs to evaluate the formula with numeric variables
// const result = evaluate(formula, variables)
// if (debug) {
// // Placeholder for logger if needed, adapting from original
// // console.debug('Money.evaluateFormula amount:', result);
// }
// // Convert the numeric result from mathjs back to a Money instance
// // This conversion from number to minor unit BigInt is where precision
// // is enforced according to the currency exponent.
// // Note: If `result` is a complex number or other non-numeric type from mathjs,
// // this conversion might fail or need additional handling.
// if (typeof result !== 'number') {
// // Handle cases where mathjs returns non-numeric results (e.g., complex numbers, matrices)
// // For a money library, non-numeric results are typically invalid.
// throw new Error(`Formula evaluation returned a non-numeric result: ${typeof result}`)
// }
// return new Money(result, currencyCode)
// } catch (error) {
// // Placeholder for logger if needed
// // console.error('Money.evaluateFormula error:', error);
// // Return zero money on error, similar to the original behavior
// return new Money(0, currencyCode)
// }
// }
// Note: Methods like `toSnapshot` and `fromSnapshot` from Dinero.js
// map to the internal state representation. With our BigInt approach,
// `toJSON` and `fromJSON` serve a similar purpose for serialization.
// Note: Methods like `toUnits` from Dinero.js break down the amount into major and minor units.
// This can be implemented by converting the BigInt amount.
/**
* Returns the amount broken down into major and minor units based on the instance's scale.
* For example, if amount is 1.23456 (with #amount = 123456n and #scale = 5), returns { major: 1n, minor: 23456n }.
* The major part carries the sign, while the minor part represents the absolute value of the fractional part.
* @returns An object containing major and minor unit amounts as BigInt.
*/
toUnits(): { major: bigint; minor: bigint } {
const divisor = BigInt(10) ** BigInt(this.#scale) // Use this.#scale for breakdown
const majorPart = this.#amount / divisor
let minorPart = this.#amount % divisor
// If the total amount is negative, the remainder (minorPart) might be negative.
// We want the minorPart to represent the absolute value of the fractional part.
if (minorPart < 0n) {
minorPart = -minorPart
}
return { major: majorPart, minor: minorPart }
}
// Note: `addPercentage` and `subtractPercentage` from the original could be implemented
// by calculating the percentage amount using `multiply` and then adding/subtracting.
/**
* Adds a percentage to the money amount.
* @param percentage The percentage to add (e.g., 10 for 10%). Can be decimal.
* @returns A new Money instance with the percentage added.
*/
addPercentage(percentage: number | string): Money {
// Calculate the percentage amount
// Treat percentage as a multiplier: amount * (percentage / 100)
const percentageAmount = this.multiply(Number(percentage) / 100)
// Add the calculated percentage amount to the original amount
return this.add(percentageAmount)
}
/**
* Subtracts a percentage from the money amount.
* @param percentage The percentage to subtract (e.g., 10 for 10%). Can be decimal.
* @returns A new Money instance with the percentage subtracted.
*/
subtractPercentage(percentage: number | string): Money {
// Calculate the percentage amount
const percentageAmount = this.multiply(Number(percentage) / 100)
// Subtract the calculated percentage amount from the original amount
return this.subtract(percentageAmount)
}
// Re-implement distributeEquallyWithTax and distributeEquallyWithTaxAboveMinimum
// based on the new allocate and arithmetic methods. This involves more complex
// allocation logic including tax and minimums, similar to the original.
/**
* Distributes the money equally with tax consideration.
* Calculates tax based on percentage, adds it to the first share, distributes the rest of the original amount.
* Optionally enforces a minimum amount for the first share.
* @param taxPercentage The percentage to add/subtract (e.g., 10 for 10%). Can be decimal.
* @param numberOfShares The number of shares to distribute into.
* @param isInclusiveTax If true, the tax percentage is already included in the initial amount. Defaults to false (exclusive).
* @param minimum Optional minimum amount (in major units) for the first share.
* @returns An array of new Money instances representing the allocated shares.
* @throws Error if numberOfShares is less than or equal to zero.
*/
distributeEquallyWithTax(
taxPercentage: number | string,
numberOfShares: number,
isInclusiveTax: boolean = false,
minimum: number | string | Money | undefined = undefined
): Money[] {
if (numberOfShares <= 0) {
throw new Error('Number of shares must be positive for equal distribution.')
}
const taxRate = Number(taxPercentage) / 100
let taxAmount: Money
let amountToDistributeEqually: Money
if (isInclusiveTax) {
// If tax is inclusive, the tax amount is part of the total.
// We need to back-calculate the tax and the net amount.
// netAmount = total / (1 + taxRate)
// taxAmount = total - netAmount
const onePlusTaxRate = 1 + taxRate
if (onePlusTaxRate === 0) {
throw new Error(
'Invalid tax rate for inclusive tax calculation (1 + taxRate cannot be zero).'
)
}
const netAmount = this.divide(onePlusTaxRate, this.#currency.exponent) // Divide by 1 + rate to get net.
taxAmount = this.subtract(netAmount) // Tax is the difference.
amountToDistributeEqually = netAmount
} else {
// If tax is exclusive, the tax amount is calculated on the total.
taxAmount = this.multiply(taxRate, this.#currency.exponent)
amountToDistributeEqually = Money.fromMinorAndScale(
this.#amount,
this.#currency.code,
this.#scale
)
}
// Distribute the `amountToDistributeEqually` equally among all shares.
const sharesBeforeTaxAllocation = amountToDistributeEqually.distributeEqually(numberOfShares)
// Add the calculated tax amount ONLY to the first share.
const allocatedShares = sharesBeforeTaxAllocation.map((share, index) =>
index === 0 ? share.add(taxAmount) : share
)
// Handle minimum logic (if applicable)
if (minimum !== undefined) {
const minimumMoney = this.getOtherMoneyForComparison(minimum)
// Check if the first calculated share (with tax) is less than the minimum.
if (allocatedShares[0].lessThan(minimumMoney)) {
const enforcedFirstShare = Money.fromMinorAndScale(
majorToMinor(minimumMoney.toDecimal(), this.#scale),
this.#currency.code,
this.#scale
)
// The remaining amount is the ORIGINAL total amount minus the enforced first share.
// This remains `this.subtract(...)` as the minimum enforcement is against the original total.
const remainingAmount = this.subtract(enforcedFirstShare)
if (numberOfShares > 1) {
const restShares = remainingAmount.distributeEqually(numberOfShares - 1)
return [enforcedFirstShare, ...restShares]
} else {
return [enforcedFirstShare]
}
}
}
return allocatedShares
}
// Inside Money class
/**
* Distributes the money equally with tax consideration, enforcing a minimum on the first share.
* Calculates tax based on percentage, adds it to the first share, distributes the rest of the amount.
* This method always applies the minimum logic.
* @param taxPercentage The percentage to add/subtract (e.g., 10 for 10%). Can be decimal.
* @param numberOfShares The number of shares to distribute into.
* @param isInclusiveTax If true, the tax percentage is already included in the initial amount. Defaults to false (exclusive).
* @param minimum The minimum amount (in major units) for the first share.
* @returns An array of new Money instances representing the allocated shares.
* @throws Error if numberOfShares is less than or equal to zero.
*/
distributeEquallyWithTaxAboveMinimum(
taxPercentage: number | string,
numberOfShares: number,
isInclusiveTax: boolean = false, // NEW PARAMETER
minimum: number | string | Money
): Money[] {
if (numberOfShares <= 0) {
throw new Error('Number of shares must be positive for equal distribution.')
}
const taxRate = Number(taxPercentage) / 100
let taxAmount: Money
let amountToDistributeEqually: Money // This will be `this` or `netAmount`
if (isInclusiveTax) {
const onePlusTaxRate = 1 + taxRate
if (onePlusTaxRate === 0) {
throw new Error(
'Invalid tax rate for inclusive tax calculation (1 + taxRate cannot be zero).'
)
}
const netAmount = this.divide(onePlusTaxRate, this.#currency.exponent)
taxAmount = this.subtract(netAmount)
amountToDistributeEqually = netAmount
} else {
taxAmount = this.multiply(taxRate, this.#currency.exponent)
amountToDistributeEqually = Money.fromMinorAndScale(
this.#amount,
this.#currency.code,
this.#scale
)
}
// Distribute the `amountToDistributeEqually` equally among all shares.
const sharesBeforeTaxAllocation = amountToDistributeEqually.distributeEqually(numberOfShares)
// Add the calculated tax amount ONLY to the first share.
const allocatedShares = sharesBeforeTaxAllocation.map((share, index) =>
index === 0 ? share.add(taxAmount) : share
)
// Handle minimum logic (which is always applied in this method)
const minimumMoney = this.getOtherMoneyForComparison(minimum)
if (allocatedShares[0].lessThan(minimumMoney)) {
const enforcedFirstShare = Money.fromMinorAndScale(
majorToMinor(minimumMoney.toDecimal(), this.#scale),
this.#currency.code,
this.#scale
)
// The remaining amount is the ORIGINAL total amount minus the enforced first share.
const remainingAmount = this.subtract(enforcedFirstShare)
if (numberOfShares > 1) {
const restShares = remainingAmount.distributeEqually(numberOfShares - 1)
return [enforcedFirstShare, ...restShares]
} else {
return [enforcedFirstShare]
}
}
return allocatedShares
}
}

Money Usage

import { Money } from './money'; // Assuming the Money class is in money.ts

// --- Usage Examples ---

// 1. Creating Money instances
console.log('--- Creating Money Instances ---');

// Create from a number (major units). Currency exponent is 2.
const amount1 = new Money(123.45, 'USD');
console.log(`Created amount1: ${amount1.toDecimal()} ${amount1.currency.code} (scale: ${amount1.scale})`);
// Expected: Created amount1: 123.45 USD (scale: 2)

// Create from a string (major units). Currency exponent is 2.
const amount2 = new Money('99.99', 'EUR');
console.log(`Created amount2: ${amount2.toDecimal()} ${amount2.currency.code} (scale: ${amount2.scale})`);
// Expected: Created amount2: 99.99 EUR (scale: 2)

// Create from a number (minor units) - use isMinorUnit: true
// ZMW has exponent 2. No explicit scale provided, so it defaults to currency exponent.
const amount3 = new Money(2500n, 'ZMW', true); // 2500 ngwee = 25 Kwacha
console.log(`Created amount3 (minor units): ${amount3.toDecimal()} ${amount3.currency.code} (scale: ${amount3.scale})`);
// Expected: Created amount3 (minor units): 25.00 ZMW (scale: 2)

// Create from another Money instance
const amount4 = new Money(amount1);
console.log(`Created amount4 from amount1: ${amount4.toDecimal()} ${amount4.currency.code} (scale: ${amount4.scale})`);
// Expected: Created amount4 from amount1: 123.45 USD (scale: 2)

// Using static helper for major units
const amount5 = Money.fromMajor(50.75, 'USD');
console.log(`Created amount5 from major: ${amount5.toDecimal()} ${amount5.currency.code} (scale: ${amount5.scale})`);
// Expected: Created amount5 from major: 50.75 USD (scale: 2)

// Using static helper for minor units
const amount6 = Money.fromMinor(10000n, 'ZMW'); // 10000 ngwee = 100 Kwacha
console.log(`Created amount6 from minor: ${amount6.toDecimal()} ${amount6.currency.code} (scale: ${amount6.scale})`);
// Expected: Created amount6 from minor: 100.00 ZMW (scale: 2)

// --- New examples demonstrating automatic scale detection and higher precision ---

// Input with more decimal places than currency exponent
const preciseAmount1 = new Money(1.23456, 'USD'); // USD exponent is 2, input has 5 decimals
console.log(`Created preciseAmount1: ${preciseAmount1.toDecimal()} ${preciseAmount1.currency.code} (scale: ${preciseAmount1.scale})`);
// Expected: Created preciseAmount1: 1.23456 USD (scale: 5)

// Input with fewer decimal places than currency exponent, scale defaults to currency exponent
const preciseAmount2 = new Money(12.5, 'ZMW'); // ZMW exponent is 2, input has 1 decimal
console.log(`Created preciseAmount2: ${preciseAmount2.toDecimal()} ${preciseAmount2.currency.code} (scale: ${preciseAmount2.scale})`);
// Expected: Created preciseAmount2: 12.50 ZMW (scale: 2)

// Example requested from prompt: 1.23456 should have minor amount 123456, scale 5
const num1 = new Money('1.23456', 'USD');
console.log(`num1: ${num1.toDecimal()} (amount: ${num1.amount}, scale: ${num1.scale})`);
// Expected: num1: 1.23456 (amount: 123456n, scale: 5)

// Example requested from prompt: 12.5 should have minor amount 1250, scale 2
const num2 = new Money('12.5', 'ZMW');
console.log(`num2: ${num2.toDecimal()} (amount: ${num2.amount}, scale: ${num2.scale})`);
// Expected: num2: 12.50 (amount: 1250n, scale: 2)


console.log('\n');

// 2. Basic Arithmetic Operations (Assumes same currency)
console.log('--- Arithmetic Operations ---');

const usd1 = new Money(100.50, 'USD');
const usd2 = new Money(50.25, 'USD');
const usdHighPrecision = new Money(10.1234, 'USD'); // Scale 4
const multiplierWithDecimals = 2.5; // Scale 1 for the multiplier value itself
const divisorWithDecimals = 4; // Treated as 4.0, scale 0

// Addition
const sum = usd1.add(usd2);
console.log(`${usd1.toDecimal()} (s:${usd1.scale}) + ${usd2.toDecimal()} (s:${usd2.scale}) = ${sum.toDecimal()} (s:${sum.scale}) ${sum.currency.code}`);
// Expected: 100.50 (s:2) + 50.25 (s:2) = 150.75 (s:2) USD

// Addition with different scales
const sumHighPrecision = usd1.add(usdHighPrecision); // Common scale will be 4
console.log(`${usd1.toDecimal()} (s:${usd1.scale}) + ${usdHighPrecision.toDecimal()} (s:${usdHighPrecision.scale}) = ${sumHighPrecision.toDecimal()} (s:${sumHighPrecision.scale}) ${sumHighPrecision.currency.code}`);
// Expected: 100.50 (s:2) + 10.1234 (s:4) = 110.6234 (s:4) USD

// Subtract
const difference = usd1.subtract(usd2);
console.log(`${usd1.toDecimal()} (s:${usd1.scale}) - ${usd2.toDecimal()} (s:${usd2.scale}) = ${difference.toDecimal()} (s:${difference.scale}) ${difference.currency.code}`);
// Expected: 100.50 (s:2) - 50.25 (s:2) = 50.25 (s:2) USD

// Subtract with different scales
const differenceHighPrecision = usdHighPrecision.subtract(usd1); // Common scale will be 4
console.log(`${usdHighPrecision.toDecimal()} (s:${usdHighPrecision.scale}) - ${usd1.toDecimal()} (s:${usd1.scale}) = ${differenceHighPrecision.toDecimal()} (s:${differenceHighPrecision.scale}) ${differenceHighPrecision.currency.code}`);
// Expected: 10.1234 (s:4) - 100.50 (s:2) = -90.3766 (s:4) USD

// Multiply by a number
const product = usd1.multiply(multiplierWithDecimals); // usd1 scale 2, multiplier scale 1. Result scale 2+1=3
console.log(`${usd1.toDecimal()} (s:${usd1.scale}) * ${multiplierWithDecimals} (s:1) = ${product.toDecimal()} (s:${product.scale}) ${product.currency.code}`);
// Expected: 100.50 (s:2) * 2.5 (s:1) = 251.250 (s:3) USD

// Divide by a number
const quotient = usd1.divide(divisorWithDecimals); // usd1 scale 2, divisor scale 0. Result scale 2 + BUFFER (e.g., 6) = 8
console.log(`${usd1.toDecimal()} (s:${usd1.scale}) / ${divisorWithDecimals} (s:0) = ${quotient.toDecimal()} (s:${quotient.scale}) ${quotient.currency.code}`);
// Expected: 100.50 (s:2) / 4 (s:0) = 25.12500000 (s:8) USD (or similar, depending on DIVISION_PRECISION_BUFFER)

// Add percentage
const amountWithTax = usd1.addPercentage(10); // 100.50 * 0.10 = 10.05. 100.50 + 10.05 = 110.55
console.log(`${usd1.toDecimal()} + 10% = ${amountWithTax.toDecimal()} (s:${amountWithTax.scale}) ${amountWithTax.currency.code}`);
// Expected: 100.50 + 10% = 110.550 (s:3) USD (because 10% translates to multiplier 0.10 which has scale 2)

// Subtract percentage
const amountAfterDiscount = usd1.subtractPercentage(5); // 100.50 * 0.05 = 5.025. 100.50 - 5.025 = 95.475
console.log(`${usd1.toDecimal()} - 5% = ${amountAfterDiscount.toDecimal()} (s:${amountAfterDiscount.scale}) ${amountAfterDiscount.currency.code}`);
// Expected: 100.50 - 5% = 95.475 (s:3) USD

console.log('\n');

// 3. Formatting
console.log('--- Formatting ---');

const zmw = new Money(1500.75, 'ZMW'); // Scale 2
const highPrecisionZMW = new Money(1500.12345, 'ZMW'); // Scale 5

// Default locale (en-ZM from user preferences), uses instance's scale
console.log(`Formatted ${zmw.toDecimal()} (default): ${zmw.format()}`);
// Expected: Formatted 1500.75 (default): K1,500.75

// Formatting with high precision, uses instance's scale
console.log(`Formatted ${highPrecisionZMW.toDecimal()} (default, high precision): ${highPrecisionZMW.format()}`);
// Expected: Formatted 1500.12345 (default, high precision): K1,500.12345

// US Dollar formatting
const usdAmount = new Money(2500.99, 'USD'); // Scale 2
console.log(`Formatted ${usdAmount.toDecimal()} (en-US): ${usdAmount.format('en-US')}`);
// Expected: Formatted 2500.99 (en-US): $2,500.99

// Euro formatting with minimumFractionDigits option
const eurAmount = new Money(1234.567, 'EUR'); // EUR exponent 2. Input 3 decimals, so scale will be 3.
console.log(`Formatted ${eurAmount.toDecimal()} (de-DE, min/max 2 digits): ${eurAmount.format('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
// Expected: Formatted 1234.567 (de-DE, min/max 2 digits): 1.234,57 € (because format options override instance scale)

console.log('\n');

// 4. Comparisons
console.log('--- Comparisons ---');

const comp1 = new Money(50, 'USD'); // Scale 2
const comp2 = new Money(100, 'USD'); // Scale 2
const comp3 = new Money(50, 'USD'); // Scale 2
const compDifferentScale = new Money('50.000', 'USD'); // Scale 3
const compDifferentCurrency = new Money(50, 'EUR'); // Scale 2

console.log(`${comp1.toDecimal()} < ${comp2.toDecimal()}? ${comp1.lessThan(comp2)}`);
// Expected: 50.00 < 100.00? true
console.log(`${comp1.toDecimal()} <= ${comp3.toDecimal()}? ${comp1.lessThanOrEqual(comp3)}`);
// Expected: 50.00 <= 50.00? true
console.log(`${comp1.toDecimal()} == ${comp3.toDecimal()}? ${comp1.equal(comp3)}`);
// Expected: 50.00 == 50.00? true (Currency must also be same)
console.log(`${comp1.toDecimal()} > ${comp2.toDecimal()}? ${comp1.greaterThan(comp2)}`);
// Expected: 50.00 > 100.00? false
console.log(`${comp2.toDecimal()} >= ${comp1.toDecimal()}? ${comp2.greaterThanOrEqual(comp1)}`);
// Expected: 100.00 >= 50.00? true

// Comparisons with different scales
console.log(`${comp1.toDecimal()} (s:${comp1.scale}) == ${compDifferentScale.toDecimal()} (s:${compDifferentScale.scale})? ${comp1.equal(compDifferentScale)}`);
// Expected: 50.00 (s:2) == 50.000 (s:3)? true (values are normalized internally)

// Comparison with numbers (treated as major units in the Money instance's currency)
console.log(`${comp1.toDecimal()} < 100? ${comp1.lessThan(100)}`);
// Expected: 50.00 < 100? true
console.log(`${comp1.toDecimal()} == 50.00? ${comp1.equal(50.00)}`);
// Expected: 50.00 == 50.00? true

// Checking signs
const negativeAmount = new Money(-25.50, 'USD');
console.log(`${negativeAmount.toDecimal()} is negative? ${negativeAmount.isNegative()}`);
// Expected: -25.50 is negative? true
console.log(`${new Money(0, 'USD').toDecimal()} is zero? ${new Money(0, 'USD').isZero()}`);
// Expected: 0.00 is zero? true
console.log(`${usd1.toDecimal()} is positive? ${usd1.isPositive()}`);
// Expected: 100.50 is positive? true
console.log(`Absolute of ${negativeAmount.toDecimal()}: ${negativeAmount.absolute().toDecimal()}`);
// Expected: Absolute of -25.50: 25.50

console.log('\n');

// 5. Allocation and Distribution
console.log('--- Allocation and Distribution ---');

const totalAmount = new Money(100, 'USD'); // Scale 2
const totalHighPrecisionAmount = new Money(100.123, 'USD'); // Scale 3

// Allocate by ratios [1, 2, 3]
const allocated = totalAmount.allocate([1, 2, 3]);
console.log(`Allocating ${totalAmount.toDecimal()} (s:${totalAmount.scale}) with ratios [1, 2, 3]:`);
allocated.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected:
// Allocating 100.00 (s:2) with ratios [1, 2, 3]:
// - 16.67 (s:2) USD
// - 33.33 (s:2) USD
// - 50.00 (s:2) USD

// Distribute equally into 4 shares
const equallyDistributed = totalHighPrecisionAmount.distributeEqually(4);
console.log(`Distributing ${totalHighPrecisionAmount.toDecimal()} (s:${totalHighPrecisionAmount.scale}) equally into 4 shares:`);
equallyDistributed.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected:
// Distributing 100.123 (s:3) equally into 4 shares:
// - 25.031 (s:3) USD
// - 25.031 (s:3) USD
// - 25.031 (s:3) USD
// - 25.030 (s:3) USD (remainder handling makes last share potentially different)

// --- distributeEquallyWithTax Examples (Exclusive vs. Inclusive Tax) ---

// Scenario 1: Exclusive Tax (default behavior)
// Total 120, 10% tax (exclusive) = 12.00 tax. Distribute 120. First share gets 12 back.
const exclusiveTaxDistribute = new Money(120, 'USD'); // Scale 2
const sharesExclusiveTax = exclusiveTaxDistribute.distributeEquallyWithTax(10, 3); // 10% tax, 3 shares, default exclusive
console.log(`\nDistributing ${exclusiveTaxDistribute.toDecimal()} with 10% EXCLUSIVE tax (3 shares):`);
sharesExclusiveTax.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected Calculation:
// Total = 120.00
// Tax = 120.00 * 0.10 = 12.00 (Calculated on Total)
// Shares BEFORE tax allocation (distribute original total) = 120.00 / 3 = 40.00 each
// First Share (gets tax added) = 40.00 + 12.00 = 52.00
// Final shares: [52.00, 40.00, 40.00]
// - 52.00 (s:2) USD
// - 40.00 (s:2) USD
// - 40.00 (s:2) USD

// Scenario 2: Exclusive Tax with Minimum Applied
const exclusiveTaxMinApplied = new Money(100, 'USD'); // Scale 2
const sharesExclusiveTaxMin = exclusiveTaxMinApplied.distributeEquallyWithTax(10, 3, false, 45); // 10% tax, 3 shares, exclusive, min 45
console.log(`\nDistributing ${exclusiveTaxMinApplied.toDecimal()} with 10% EXCLUSIVE tax and min 45 (3 shares):`);
sharesExclusiveTaxMin.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected Calculation:
// Total = 100.00
// Tax = 100.00 * 0.10 = 10.00 (Calculated on Total)
// Shares BEFORE tax allocation = 100.00 / 3 = 33.33 (approx)
// First Share (gets tax added) = 33.33 + 10.00 = 43.33
// Is 43.33 < minimum (45)? Yes. So minimum logic IS triggered.
// Enforced First Share = 45.00
// Remaining Amount = Total (100.00) - Enforced First Share (45.00) = 55.00
// Distribute Remaining (55.00) among remaining shares (3 - 1 = 2 shares).
// Rest Shares = 55.00 / 2 = 27.50 each
// Final shares: [45.00, 27.50, 27.50]
// - 45.00 (s:2) USD
// - 27.50 (s:2) USD
// - 27.50 (s:2) USD


// Scenario 3: Inclusive Tax
// Total 120, 10% tax (inclusive).
// Net = 120 / (1 + 0.10) = 120 / 1.10 = 109.0909...
// Tax Amount = 120 - 109.09 = 10.91 (approx)
// Distribute 109.09. First share gets 10.91 back.
const inclusiveTaxDistribute = new Money(120, 'USD'); // Scale 2
const sharesInclusiveTax = inclusiveTaxDistribute.distributeEquallyWithTax(10, 3, true); // 10% tax, 3 shares, inclusive
console.log(`\nDistributing ${inclusiveTaxDistribute.toDecimal()} with 10% INCLUSIVE tax (3 shares):`);
sharesInclusiveTax.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected Calculation:
// Total = 120.00
// Net Amount to Distribute = 120 / 1.10 = 109.09 (rounded for display)
// Tax Amount = 120 - 109.09 = 10.91 (rounded for display)
// Shares BEFORE tax allocation (distribute NET amount) = 109.09 / 3 = 36.36 (approx) each
// First Share (gets tax added) = 36.36 + 10.91 = 47.27 (approx)
// Final shares (due to rounding, amounts may vary slightly):
// - 47.27 (s:2) USD
// - 36.36 (s:2) USD
// - 36.36 (s:2) USD

// Scenario 4: Inclusive Tax with Minimum Applied
const inclusiveTaxMinApplied = new Money(110, 'USD'); // Scale 2
const sharesInclusiveTaxMin = inclusiveTaxMinApplied.distributeEquallyWithTax(10, 3, true, 40); // 10% tax, 3 shares, inclusive, min 40
console.log(`\nDistributing ${inclusiveTaxMinApplied.toDecimal()} with 10% INCLUSIVE tax and min 40 (3 shares):`);
sharesInclusiveTaxMin.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected Calculation:
// Total = 110.00
// Net Amount to Distribute = 110 / 1.10 = 100.00
// Tax Amount = 110 - 100 = 10.00
// Shares BEFORE tax allocation = 100.00 / 3 = 33.33 (approx) each
// First Share (gets tax added) = 33.33 + 10.00 = 43.33
// Is 43.33 < minimum (40)? No.
// Final shares: [43.33, 33.33, 33.34] (or similar, due to rounding in 33.33 / 33.34 split)
// - 43.33 (s:2) USD
// - 33.33 (s:2) USD
// - 33.34 (s:2) USD

console.log('\n');

// 6. Currency Conversion (Basic implementation - requires external rates in a real app)
console.log('--- Currency Conversion ---');

// Note: This requires defining the target currency in the `currencies` registry
// and providing a manual conversion rate. A real-world scenario would fetch rates.

const usdToEurRate = 0.92; // Example: 1 USD = 0.92 EUR (scale of rate is 2)

try {
    const usdToConvert = new Money(100, 'USD'); // Scale 2
    const convertedEur = usdToConvert.convert('EUR', usdToEurRate);
    console.log(`${usdToConvert.toDecimal()} (s:${usdToConvert.scale}) ${usdToConvert.currency.code} converted to EUR: ${convertedEur.toDecimal()} (s:${convertedEur.scale}) ${convertedEur.currency.code}`);
    // Expected (100.00 USD * 0.92 EUR/USD = 92.00 EUR): 100.00 (s:2) USD converted to EUR: 92.00 (s:2) EUR
} catch (e: any) {
    console.error(`Conversion failed: ${e.message}`);
}

try {
    const usdToConvertHighPrec = new Money(100.123, 'USD'); // Scale 3
    const eurRateHighPrec = '0.92123'; // Scale 5
    const convertedEurHighPrec = usdToConvertHighPrec.convert('EUR', eurRateHighPrec);
    console.log(`${usdToConvertHighPrec.toDecimal()} (s:${usdToConvertHighPrec.scale}) ${usdToConvertHighPrec.currency.code} converted to EUR: ${convertedEurHighPrec.toDecimal()} (s:${convertedEurHighPrec.scale}) ${convertedEurHighPrec.currency.code}`);
    // Expected (100.123 * 0.92123 = 92.2346929 -> rounded to EUR exponent 2): 100.123 (s:3) USD converted to EUR: 92.23 (s:2) EUR
} catch (e: any) {
    console.error(`Conversion failed: ${e.message}`);
}


console.log('\n');

// 7. toDecimal and toNumber
console.log('--- toDecimal and toNumber ---');

const preciseAmount = new Money(123.4567, 'USD'); // USD exponent 2. Input 4 decimals.
// Constructor sets #scale to 4. majorToMinor will result in 1234567n.

console.log(`Original number: 123.4567`);
console.log(`Stored amount (minor units): ${preciseAmount.amount}`);
// Expected: Stored amount (minor units): 1234567n

console.log(`Money instance scale: ${preciseAmount.scale}`);
// Expected: Money instance scale: 4

console.log(`toDecimal(): ${preciseAmount.toDecimal()}`);
// Expected: toDecimal(): 123.4567 (Uses instance's scale)

console.log(`toNumber(): ${preciseAmount.toNumber()}`);
// Expected: toNumber(): 123.4567 (Converts toDecimal() string to number)

console.log('\n');

// 8. Minimum and Maximum
console.log('--- Minimum and Maximum ---');

const minMax1 = new Money(20, 'USD'); // Scale 2
const minMax2 = new Money(50, 'USD'); // Scale 2
const minMax3 = new Money(10, 'USD'); // Scale 2
const minMax4 = new Money('10.000', 'USD'); // Scale 3

const minimumAmount = minMax1.minimum(minMax2, minMax3, minMax4);
console.log(`Minimum of ${minMax1.toDecimal()}, ${minMax2.toDecimal()}, ${minMax3.toDecimal()}, ${minMax4.toDecimal()}: ${minimumAmount.toDecimal()} (s:${minimumAmount.scale}) ${minimumAmount.currency.code}`);
// Expected: Minimum of 20.00, 50.00, 10.00, 10.000: 10.00 (s:2) USD (Returns the original object found to be minimum)

const maximumAmount = minMax1.maximum(minMax2, minMax3, minMax4);
console.log(`Maximum of ${minMax1.toDecimal()}, ${minMax2.toDecimal()}, ${minMax3.toDecimal()}, ${minMax4.toDecimal()}: ${maximumAmount.toDecimal()} (s:${maximumAmount.scale}) ${maximumAmount.currency.code}`);
// Expected: Maximum of 20.00, 50.00, 10.00, 10.000: 50.00 (s:2) USD

// Minimum/Maximum with numbers
const minMaxWithNumber = minMax1.minimum(5.00, 15.00);
console.log(`Minimum of ${minMax1.toDecimal()}, 5.00, 15.00: ${minMaxWithNumber.toDecimal()} (s:${minMaxWithNumber.scale}) ${minMaxWithNumber.currency.code}`);
// Expected: Minimum of 20.00, 5.00, 15.00: 5.00 (s:2) USD


console.log('\n');

// 9. Same Amount / Same Currency
console.log('--- Same Amount / Same Currency ---');

const same1 = new Money(100, 'USD'); // Scale 2
const same2 = new Money(100, 'USD'); // Scale 2
const same3 = new Money(100, 'EUR'); // Scale 2
const same4 = new Money(50, 'USD'); // Scale 2
const same5 = new Money('100.000', 'USD'); // Scale 3

console.log(`${same1.toDecimal()} (s:${same1.scale}) and ${same2.toDecimal()} (s:${same2.scale}) have same amount? ${same1.haveSameAmount(same2)}`);
// Expected: 100.00 (s:2) and 100.00 (s:2) have same amount? true
console.log(`${same1.toDecimal()} (s:${same1.scale}) and ${same3.toDecimal()} (s:${same3.scale}) have same amount? ${same1.haveSameAmount(same3)}`);
// Expected: 100.00 (s:2) and 100.00 (s:2) have same amount? true (Compares decimal values, ignores currency)
console.log(`${same1.toDecimal()} (s:${same1.scale}) and ${same5.toDecimal()} (s:${same5.scale}) have same amount? ${same1.haveSameAmount(same5)}`);
// Expected: 100.00 (s:2) and 100.000 (s:3) have same amount? true (Values are normalized for comparison)
console.log(`${same1.toDecimal()} (s:${same1.scale}) and ${same4.toDecimal()} (s:${same4.scale}) have same amount? ${same1.haveSameAmount(same4)}`);
// Expected: 100.00 (s:2) and 50.00 (s:2) have same amount? false

console.log(`${same1.toDecimal()} and ${same2.toDecimal()} have same currency? ${same1.haveSameCurrency(same2)}`);
// Expected: 100.00 and 100.00 have same currency? true
console.log(`${same1.toDecimal()} and ${same3.toDecimal()} have same currency? ${same1.haveSameCurrency(same3)}`);
// Expected: 100.00 and 100.00 have same currency? false


console.log('\n');

// 10. Has SubUnits
console.log('--- Has SubUnits ---');

const wholeAmount = new Money(100, 'USD'); // Stored as 10000n at scale 2
const fractionalAmount = new Money(100.50, 'USD'); // Stored as 10050n at scale 2
const exactFractionalAmount = new Money(100.00, 'USD'); // Stored as 10000n at scale 2
const ultraPreciseAmount = new Money(100.12345, 'USD'); // Stored as 10012345n at scale 5

console.log(`${wholeAmount.toDecimal()} has subunits? ${wholeAmount.hasSubUnits()}`);
// Expected: 100.00 has subunits? false (Remainder when divided by 10^2 (USD exponent) is 0)

console.log(`${fractionalAmount.toDecimal()} has subunits? ${fractionalAmount.hasSubUnits()}`);
// Expected: 100.50 has subunits? true (Remainder when divided by 10^2 is 50n)

console.log(`${exactFractionalAmount.toDecimal()} has subunits? ${exactFractionalAmount.hasSubUnits()}`);
// Expected: 100.00 has subunits? false (Remainder when divided by 10^2 is 0)

console.log(`${ultraPreciseAmount.toDecimal()} has subunits? ${ultraPreciseAmount.hasSubUnits()}`);
// Expected: 100.12345 has subunits? true (Remainder when divided by 10^2 is non-zero, e.g., 12345n % 100n != 0)


console.log('\n');

// 11. Formula Evaluation (Using mathjs - see notes about precision)
console.log('--- Formula Evaluation ---');

// Note: The `evaluateFormula` method in the generated code uses `mathjs`,
// which typically operates on numbers or its own BigNumber type, not necessarily the BigInt
// used internally by the Money class for storage. Precision might be limited by mathjs's capabilities.
// This example assumes `evaluateFormula` is uncommented in your Money class and `mathjs` is installed.

/*
// Example: Calculate (a + b) * c
const formulaResult = Money.evaluateFormula("(a + b) * c", { a: 10.50, b: 20.25, c: 2 }, 'USD');
console.log(`Formula "(a + b) * c" with a=10.50, b=20.25, c=2 = ${formulaResult.toDecimal()} ${formulaResult.currency.code}`);
// Expected: Formula "(a + b) * c" with a=10.50, b=20.25, c=2 = 61.50 USD
// Calculation: (10.50 + 20.25) * 2 = 30.75 * 2 = 61.50

// Example with slightly more complex numbers (mathjs might handle this)
const formulaResult2 = Money.evaluateFormula("x / y + z", { x: 100, y: 3, z: 10 }, 'USD');
console.log(`Formula "x / y + z" with x=100, y=3, z=10 = ${formulaResult2.toDecimal()} ${formulaResult2.currency.code}`);
// Expected: Formula "x / y + z" with x=100, y=3, z=10 = 43.33 USD (Mathjs will calculate 100/3 as ~33.333..., add 10, result ~43.333..., rounded to 2 decimals for USD)

// Example with invalid formula or variables
const invalidFormulaResult = Money.evaluateFormula("a + ", { a: 10 }, 'USD');
console.log(`Invalid formula "a + ": ${invalidFormulaResult.toDecimal()} ${invalidFormulaResult.currency.code}`);
// Expected: Invalid formula "a + ": 0.00 USD (Error is caught and returns 0)
*/

console.log('\n');

// 12. transformScale (Use with caution)
console.log('--- transformScale ---');

const originalAmount = new Money(123.45, 'USD'); // USD exponent 2. Input 2 decimals.
// Constructor creates: #amount = 12345n, #scale = 2.
console.log(`Original amount: ${originalAmount.toDecimal()} (current scale: ${originalAmount.scale}, currency exponent: ${originalAmount.currency.exponent})`);
// Expected: Original amount: 123.45 (current scale: 2, currency exponent: 2)

// Transform to exponent 0 (major units as the minor unit)
const transformedToExponent0 = originalAmount.transformScale(0);
console.log(`Transformed to exponent 0: ${transformedToExponent0.toDecimal()} (new scale: ${transformedToExponent0.scale})`);
console.log(`Internal amount BigInt: ${transformedToExponent0.amount}`);
// Expected (123.45 rounded to 0 decimal places is 123): Transformed to exponent 0: 123 (new scale: 0)
// Expected: Internal amount BigInt: 123n

// Transform back to exponent 2 from exponent 0 (will not recover lost precision)
const transformedBackToExponent2 = transformedToExponent0.transformScale(2);
console.log(`Transformed back to exponent 2: ${transformedBackToExponent2.toDecimal()} (new scale: ${transformedBackToExponent2.scale})`);
console.log(`Internal amount BigInt: ${transformedBackToExponent2.amount}`);
// Expected: Transformed back to exponent 2: 123.00 (new scale: 2)
// Expected: Internal amount BigInt: 12300n

// Transform to exponent 3 (milli-dollars) - scales up
const transformedToExponent3 = originalAmount.transformScale(3);
console.log(`Transformed to exponent 3: ${transformedToExponent3.toDecimal()} (new scale: ${transformedToExponent3.scale})`);
console.log(`Internal amount BigInt: ${transformedToExponent3.amount}`);
// Expected: Transformed to exponent 3: 123.450 (new scale: 3)
// Expected: Internal amount BigInt: 123450n

// Transform to exponent 1 for an amount with higher initial precision, demonstrating rounding
const preciseInitial = new Money(1.23456, 'USD'); // #amount = 123456n, #scale = 5
console.log(`\nOriginal precise: ${preciseInitial.toDecimal()} (current scale: ${preciseInitial.scale})`);
// Expected: Original precise: 1.23456 (current scale: 5)
const transformedToExponent1 = preciseInitial.transformScale(1); // Scale down from 5 to 1
console.log(`Transformed to exponent 1: ${transformedToExponent1.toDecimal()} (new scale: ${transformedToExponent1.scale})`);
console.log(`Internal amount BigInt: ${transformedToExponent1.amount}`);
// Expected (1.23456 rounded to 1 decimal place is 1.2): Transformed to exponent 1: 1.2 (new scale: 1)
// Expected: Internal amount BigInt: 12n (representing 1.2)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment