Last active
May 23, 2025 15:21
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Represents a monetary value using BigInt for precision. | |
*/ | |
export interface Currency { | |
code: string; | |
exponent: number; // Number of decimal places in the minor unit | |
} | |
// A simple registry of supported currencies | |
const currencies: { [key: string]: Currency } = { | |
ZMW: { code: 'ZMW', exponent: 2 }, // Zambian Kwacha | |
USD: { code: 'USD', exponent: 2 }, // US Dollar | |
EUR: { code: 'EUR', exponent: 2 }, // Euro | |
// Add other currencies as needed | |
}; | |
/** | |
* Converts a major unit number to a BigInt minor unit based on currency exponent. | |
* @param amount Major unit amount (e.g., 12.34) | |
* @param exponent Currency exponent (e.g., 2 for cents) | |
* @returns Amount in minor units as BigInt | |
*/ | |
function majorToMinor(amount: number | string, exponent: number): bigint { | |
const [integerPart, decimalPart] = String(amount).split('.'); | |
const decimalPlaces = decimalPart ? decimalPart.length : 0; | |
const integerBigInt = BigInt(integerPart || '0'); | |
const decimalBigInt = BigInt(decimalPart || '0'); | |
// Calculate the factor to shift the decimal part to the minor unit scale | |
const decimalFactor = BigInt(10) ** BigInt(exponent - decimalPlaces); | |
// Calculate the factor to shift the integer part to the minor unit scale | |
const integerFactor = BigInt(10) ** BigInt(exponent); | |
return (integerBigInt * integerFactor) + (decimalBigInt * decimalFactor); | |
} | |
/** | |
* Converts a BigInt minor unit amount back to a number representing the major unit. | |
* Note: This can lose precision and should primarily be used for display or compatibility layers. | |
* @param amount Minor unit amount as BigInt | |
* @param exponent Currency exponent | |
* @returns Amount in major units as a number | |
*/ | |
function minorToMajor(amount: bigint, exponent: number): number { | |
// Dividing BigInt by 10^exponent gives the integer part in major units. | |
// To get the decimal part, calculate the remainder and convert it. | |
const divisor = BigInt(10) ** BigInt(exponent); | |
const majorPart = amount / divisor; | |
const minorPart = amount % divisor; | |
// Convert minor part to a decimal string and pad with leading zeros if necessary | |
const minorString = minorPart.toString().padStart(exponent, '0'); | |
// Combine integer and decimal parts | |
const resultString = `${majorPart}.${minorString}`; | |
// Convert the string back to a number | |
return parseFloat(resultString); | |
} | |
/** | |
* 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; | |
/** | |
* Creates a new Money instance. | |
* @param amount The amount in major units (e.g., 12.34) or a Money instance. | |
* @param currencyCode The currency code (default: 'ZMW'). | |
* @param isMinorUnit If true, the amount is treated as minor units directly. | |
*/ | |
constructor(amount: number | string | Money, currencyCode: string = 'ZMW', isMinorUnit: boolean = false) { | |
const currency = currencies[currencyCode]; | |
if (!currency) { | |
throw new Error(`Unsupported currency code: ${currencyCode}`); | |
} | |
this.#currency = currency; | |
if (amount instanceof Money) { | |
// Ensure currency compatibility or handle conversion | |
if (amount.currency.code !== this.#currency.code) { | |
// For simplicity, throw error for now. Conversion would require rates. | |
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; | |
} else { | |
if (isMinorUnit) { | |
// Treat the number/string directly as minor units (BigInt) | |
this.#amount = BigInt(amount); | |
} else { | |
// Convert major unit number/string to minor unit BigInt | |
this.#amount = majorToMinor(amount, this.#currency.exponent); | |
} | |
} | |
} | |
/** | |
* Gets the amount in the smallest currency unit as BigInt. | |
*/ | |
get amount(): bigint { | |
return this.#amount; | |
} | |
/** | |
* Gets the currency of this Money instance. | |
*/ | |
get currency(): Currency { | |
return this.#currency; | |
} | |
/** | |
* Adds another Money instance or a number to this Money instance. | |
* Assumes the same currency for Money operands. | |
* Adding a number treats it as a major unit value in the current currency. | |
* @param addend The amount to add. | |
* @returns A new Money instance with the result. | |
*/ | |
add(addend: Money | number | string): Money { | |
let addendAmount: bigint; | |
if (addend instanceof Money) { | |
if (addend.currency.code !== this.#currency.code) { | |
throw new Error(`Cannot add Money instances with different currencies (${this.#currency.code} and ${addend.currency.code}).`); | |
} | |
addendAmount = addend.amount; | |
} else { | |
// Treat number/string as major units in the current currency | |
addendAmount = majorToMinor(addend, this.#currency.exponent); | |
} | |
return new Money(this.#amount + addendAmount, this.#currency.code, true); // Use true for isMinorUnit | |
} | |
/** | |
* Subtracts another Money instance or a number from this Money instance. | |
* Assumes the same currency for Money operands. | |
* Subtracting a number treats it as a major unit value in the current currency. | |
* @param subtrahend The amount to subtract. | |
* @returns A new Money instance with the result. | |
*/ | |
subtract(subtrahend: Money | number | string): Money { | |
let subtrahendAmount: bigint; | |
if (subtrahend instanceof Money) { | |
if (subtrahend.currency.code !== this.#currency.code) { | |
throw new Error(`Cannot subtract Money instances with different currencies (${this.#currency.code} and ${subtrahend.currency.code}).`); | |
} | |
subtrahendAmount = subtrahend.amount; | |
} else { | |
// Treat number/string as major units in the current currency | |
subtrahendAmount = majorToMinor(subtrahend, this.#currency.exponent); | |
} | |
return new Money(this.#amount - subtrahendAmount, this.#currency.code, true); // Use true for isMinorUnit | |
} | |
/** | |
* Multiplies this Money instance by a number. | |
* Note: Multiplication of Money by Money is not supported as per standard money arithmetic. | |
* @param multiplier The number to multiply by. | |
* @returns A new Money instance with the result. | |
*/ | |
multiply(multiplier: number | string): Money { | |
// Convert multiplier to a BigInt, handling decimals if necessary | |
// For simplicity, treat multiplier as a fixed-point number scaled by its decimal places. | |
// A more robust solution might use a Big Decimal library or handle scaling carefully. | |
const [integerPart, decimalPart] = String(multiplier).split('.'); | |
const decimalPlaces = decimalPart ? decimalPart.length : 0; | |
const multiplierBigInt = BigInt(`${integerPart}${decimalPart || ''}`); | |
// Scale the current amount by the multiplier BigInt. | |
// Then, scale down by 10^decimalPlaces of the multiplier to correct the magnitude. | |
const scaledAmount = this.#amount * multiplierBigInt; | |
const divisor = BigInt(10) ** BigInt(decimalPlaces); | |
// Implement rounding strategy if needed here. For simplicity, using integer division. | |
const resultAmount = scaledAmount / divisor; | |
return new Money(resultAmount, this.#currency.code, true); // Use true for isMinorUnit | |
} | |
/** | |
* Divides this Money instance by a number. | |
* Returns a new Money instance. | |
* @param divisor The number to divide by. Must not be zero. | |
* @returns A new Money instance with the result. | |
* @throws Error if divisor is zero. | |
*/ | |
divide(divisor: number | string): Money { | |
const divisorNum = Number(divisor); | |
if (divisorNum === 0) { | |
throw new Error("Division by zero."); | |
} | |
// Convert divisor to a BigInt, handling decimals | |
const [integerPart, decimalPart] = String(divisor).split('.'); | |
const decimalPlaces = decimalPart ? decimalPart.length : 0; | |
const divisorBigInt = BigInt(`${integerPart}${decimalPart || ''}`); | |
if (divisorBigInt === 0n) { | |
throw new Error("Division by zero."); | |
} | |
// To maintain precision during division, scale up the amount before dividing. | |
// Scale up by the decimal places of the divisor. | |
const scaledAmount = this.#amount * (BigInt(10) ** BigInt(decimalPlaces)); | |
// Perform the division | |
const resultAmount = scaledAmount / divisorBigInt; // Integer division | |
// Note: This performs integer division after scaling. | |
// For more precise fractional results, a different approach or rounding strategy is needed. | |
// This implementation assumes integer division on scaled minor units. | |
return new Money(resultAmount, this.#currency.code, true); // Use true for isMinorUnit | |
} | |
/** | |
* Allocates the money into multiple shares based on ratios. | |
* Note: This implementation uses BigInt division, which truncates. | |
* More sophisticated allocation might involve distributing remainders. | |
* @param ratios An array of numbers representing the ratios for allocation. | |
* @returns An array of new Money instances representing the allocated shares. | |
*/ | |
allocate(ratios: number[]): 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; // Track the sum of allocated minor units | |
for (let i = 0; i < ratios.length; i++) { | |
const ratio = BigInt(Math.round(ratios[i] * 1000)); // Scale ratio to maintain some precision | |
const totalRatioScaled = BigInt(Math.round(totalRatio * 1000)); | |
// Calculate share using scaled ratio and the total amount (BigInt) | |
// Perform multiplication before division to minimize loss of precision | |
let share = (this.#amount * ratio) / totalRatioScaled; | |
allocatedAmounts.push(share); | |
totalAllocated += share; | |
} | |
// Handle potential remainder due to BigInt integer division | |
const remainder = this.#amount - totalAllocated; | |
// Distribute the remainder among the first 'remainder' shares | |
for (let i = 0; i < remainder; i++) { | |
if (i < allocatedAmounts.length) { | |
allocatedAmounts[i]++; | |
} else { | |
// This case should ideally not happen with correct remainder calculation | |
console.warn("Allocation remainder exceeds number of shares."); | |
} | |
} | |
return allocatedAmounts.map(amount => new Money(amount, this.#currency.code, true)); // Use true for isMinorUnit | |
} | |
/** | |
* Distributes the money equally into a specified number of shares. | |
* @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."); | |
} | |
// Create ratios for equal distribution (e.g., [1, 1, 1] for 3 shares) | |
const ratios = Array(numberOfShares).fill(1); | |
return this.allocate(ratios); | |
} | |
/** | |
* 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. | |
const majorUnitAmount = minorToMajor(this.#amount, this.#currency.exponent); | |
return formatter.format(majorUnitAmount); | |
} | |
/** | |
* Returns the amount in major units as a number. | |
* Note: This conversion can lead to floating-point inaccuracies for display purposes. | |
* Use `format` for localized display. | |
* @returns The amount in major units as a number. | |
*/ | |
toNumber(): number { | |
return minorToMajor(this.#amount, this.#currency.exponent); | |
} | |
/** | |
* Returns the amount in major units as a string. | |
* This avoids potential floating-point issues of `toNumber()`. | |
* @returns The amount in major units as a string. | |
*/ | |
toDecimal(): string { | |
const divisor = BigInt(10) ** BigInt(this.#currency.exponent); | |
const majorPart = this.#amount / divisor; | |
const minorPart = this.#amount % divisor; | |
// Pad the minor part with leading zeros to match the exponent | |
const minorString = minorPart.toString().padStart(this.#currency.exponent, '0'); | |
// Handle negative numbers | |
if (this.#amount < 0n) { | |
return `-${majorPart.toString().replace('-', '')}.${minorString}`; | |
} | |
return `${majorPart}.${minorString}`; | |
} | |
/** | |
* Checks if this Money instance is less than another. | |
* @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 { | |
let otherAmount: bigint; | |
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}).`); | |
} | |
otherAmount = other.amount; | |
} else { | |
otherAmount = majorToMinor(other, this.#currency.exponent); | |
} | |
return this.#amount < otherAmount; | |
} | |
/** | |
* Checks if this Money instance is less than or equal to another. | |
* @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 { | |
let otherAmount: bigint; | |
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}).`); | |
} | |
otherAmount = other.amount; | |
} else { | |
otherAmount = majorToMinor(other, this.#currency.exponent); | |
} | |
return this.#amount <= otherAmount; | |
} | |
/** | |
* Checks if this Money instance is equal to another. | |
* Currency must also be the same. | |
* @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 { | |
let otherAmount: bigint; | |
if (other instanceof Money) { | |
if (other.currency.code !== this.#currency.code) { | |
// Equal check also requires same currency | |
return false; | |
} | |
otherAmount = other.amount; | |
} else { | |
otherAmount = majorToMinor(other, this.#currency.exponent); | |
} | |
return this.#amount === otherAmount; | |
} | |
/** | |
* Checks if this Money instance is greater than or equal to another. | |
* @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 { | |
let otherAmount: bigint; | |
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}).`); | |
} | |
otherAmount = other.amount; | |
} else { | |
otherAmount = majorToMinor(other, this.#currency.exponent); | |
} | |
return this.#amount >= otherAmount; | |
} | |
/** | |
* Checks if this Money instance is greater than another. | |
* @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 { | |
let otherAmount: bigint; | |
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}).`); | |
} | |
otherAmount = other.amount; | |
} else { | |
otherAmount = majorToMinor(other, this.#currency.exponent); | |
} | |
return this.#amount > otherAmount; | |
} | |
/** | |
* 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. | |
*/ | |
absolute(): Money { | |
return new Money(this.#amount > 0n ? this.#amount : -this.#amount, this.#currency.code, true); | |
} | |
/** | |
* Finds the minimum among this Money instance and others. | |
* All instances must have the same currency. | |
* @param others Other Money instances or numbers to compare. | |
* @returns The minimum Money instance. | |
* @throws Error if currencies differ. | |
*/ | |
minimum(...others: Array<Money | number | string>): Money { | |
let minAmount = this.#amount; | |
for (const other of others) { | |
let otherAmount: bigint; | |
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}).`); | |
} | |
otherAmount = other.amount; | |
} else { | |
otherAmount = majorToMinor(other, this.#currency.exponent); | |
} | |
if (otherAmount < minAmount) { | |
minAmount = otherAmount; | |
} | |
} | |
return new Money(minAmount, this.#currency.code, true); | |
} | |
/** | |
* Finds the maximum among this Money instance and others. | |
* All instances must have the same currency. | |
* @param others Other Money instances or numbers to compare. | |
* @returns The maximum Money instance. | |
* @throws Error if currencies differ. | |
*/ | |
maximum(...others: Array<Money | number | string>): Money { | |
let maxAmount = this.#amount; | |
for (const other of others) { | |
let otherAmount: bigint; | |
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}).`); | |
} | |
otherAmount = other.amount; | |
} else { | |
otherAmount = majorToMinor(other, this.#currency.exponent); | |
} | |
if (otherAmount > maxAmount) { | |
maxAmount = otherAmount; | |
} | |
} | |
return new Money(maxAmount, this.#currency.code, true); | |
} | |
/** | |
* Checks if this Money instance and others have the same amount. | |
* Currency is ignored for this check. | |
* @param others Other Money instances or numbers to compare. | |
* @returns True if all instances have the same amount (BigInt minor unit value). | |
*/ | |
haveSameAmount(...others: Array<Money | number | string>): boolean { | |
let currentAmount = this.#amount; | |
for (const other of others) { | |
let otherAmount: bigint; | |
if (other instanceof Money) { | |
// When comparing amounts, currency is ignored. | |
// Need to convert 'other' amount to the current instance's minor unit scale | |
// based on its *own* currency exponent, then potentially scale it | |
// to compare directly as BigInts. This is complex and might require conversion logic. | |
// For a simpler implementation assuming comparison of the raw minor unit BigInt: | |
// This assumes the minor unit value itself is what's being compared, | |
// regardless of currency exponent, which might not be the desired behavior. | |
// A more accurate comparison would involve normalizing to a common scale or currency. | |
// Based on the Dinero API, "haveSameAmount" compares amounts regardless of currency. | |
// This means we should compare the numerical value represented, | |
// likely requiring conversion to a common representation or comparing the | |
// major unit values as precisely as possible. | |
// Given the BigInt approach, comparing the raw minor unit amounts is the easiest, | |
// but semantically questionable across different currencies. | |
// Let's reinterpret "haveSameAmount" to mean comparing the numeric value | |
// when converted to a common representation (e.g., a string decimal). | |
// This avoids direct BigInt comparison across different scales. | |
if (this.toDecimal() !== other.toDecimal()) { | |
return false; | |
} | |
} else { | |
// Compare current instance's decimal string with the decimal string of the number/string | |
if (this.toDecimal() !== new Money(other, this.#currency.code, false).toDecimal()) { | |
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. | |
* Requires a conversion rate. | |
* @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 rate = Number(conversionRate); | |
if (isNaN(rate) || rate <= 0) { | |
throw new Error("Invalid conversion rate."); | |
} | |
if (this.#currency.code === targetCurrency.code) { | |
return this; // No conversion needed if already in target currency | |
} | |
// Convert current amount to a base unit (like a common intermediate currency, or just major units) | |
// Multiply by the conversion rate | |
// Convert the result to the target currency's minor units | |
// A simpler approach: | |
// Convert current amount to major units (can lose precision here if not careful) | |
// Or, multiply the current minor unit BigInt by the rate, carefully handling decimal places of the rate | |
// Then scale the result to the target currency's exponent | |
// Let's multiply the minor unit amount by the conversion rate, handling rate's decimal places. | |
const [rateIntegerPart, rateDecimalPart] = String(conversionRate).split('.'); | |
const rateDecimalPlaces = rateDecimalPart ? rateDecimalPart.length : 0; | |
const rateBigInt = BigInt(`${rateIntegerPart}${rateDecimalPart || ''}`); | |
// Scale the current amount by the conversion rate BigInt | |
const scaledAmount = this.#amount * rateBigInt; | |
// Now, adjust the scale based on the rate's decimal places and the difference in currency exponents | |
// Current amount is at `this.#currency.exponent` scale. | |
// We multiplied by a rate scaled by `rateDecimalPlaces`. So the result is at scale `this.#currency.exponent + rateDecimalPlaces`. | |
// We need to reach the `targetCurrency.exponent` scale. | |
// Difference in scale adjustment needed: `(this.#currency.exponent + rateDecimalPlaces) - targetCurrency.exponent` | |
const currentScale = BigInt(this.#currency.exponent); | |
const targetScale = BigInt(targetCurrency.exponent); | |
const rateScale = BigInt(rateDecimalPlaces); | |
const requiredScaleDifference = (currentScale + rateScale) - targetScale; | |
let convertedAmount: bigint; | |
if (requiredScaleDifference > 0n) { | |
// Need to scale down | |
const divisor = BigInt(10) ** requiredScaleDifference; | |
convertedAmount = scaledAmount / divisor; // Integer division (rounding down) | |
// Consider rounding strategies if needed here (e.g., add divisor/2 before division for rounding to nearest) | |
} else if (requiredScaleDifference < 0n) { | |
// Need to scale up | |
const factor = BigInt(10) ** -requiredScaleDifference; | |
convertedAmount = scaledAmount * factor; | |
} else { | |
// No scale adjustment needed relative to the rate's scaling | |
convertedAmount = scaledAmount; | |
} | |
return new Money(convertedAmount, targetCurrency.code, true); // Use true for isMinorUnit | |
} | |
// 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. | |
* @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.#currency.exponent); | |
const targetExponent = BigInt(newExponent); | |
if (targetExponent === currentExponent) { | |
return this; // No scale change needed | |
} | |
const exponentDifference = targetExponent - currentExponent; | |
let transformedAmount: bigint; | |
let newCurrency: Currency = { ...this.#currency, exponent: newExponent }; // Create a new currency representation with the new exponent | |
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. | |
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. | |
const divisor = BigInt(10) ** -exponentDifference; | |
// Integer division here will truncate. A proper implementation might need rounding. | |
transformedAmount = this.#amount / divisor; | |
} | |
// Create a new Money instance with the transformed amount and the new currency representation. | |
return new Money(transformedAmount, newCurrency.code, true); // Amount is already in the units of the new exponent | |
} | |
// JSON serialization | |
toJSON(): { amount: string; currency: Currency } { | |
return { | |
amount: this.#amount.toString(), // Store BigInt as string in JSON | |
currency: this.#currency, | |
}; | |
} | |
static fromJSON(json: { amount: string; currency: Currency }): Money { | |
return new Money(BigInt(json.amount), json.currency.code, true); // Amount is minor unit BigInt | |
} | |
// Example of a static helper to create Money from major unit number | |
static fromMajor(amount: number | string, currencyCode: string = 'ZMW'): Money { | |
return new Money(amount, currencyCode, false); | |
} | |
// Example of a static helper to create Money from minor unit BigInt | |
static fromMinor(amount: bigint, currencyCode: string = 'ZMW'): Money { | |
return new Money(amount, currencyCode, true); | |
} | |
// --- 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. | |
* @returns An object containing major and minor unit amounts as BigInt. | |
*/ | |
toUnits(): { major: bigint; minor: bigint } { | |
const divisor = BigInt(10) ** BigInt(this.#currency.exponent); | |
const majorPart = this.#amount / divisor; | |
const minorPart = this.#amount % divisor; | |
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, subtracts it, distributes the net amount equally, | |
* and adds the tax back to the first share. Optionally enforces a minimum amount for the first share. | |
* @param taxPercentage The percentage of tax to apply (e.g., 10 for 10%). Can be decimal. | |
* @param numberOfShares The number of shares to distribute into. | |
* @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, | |
minimum: number | string | Money | undefined = undefined, | |
): Money[] { | |
if (numberOfShares <= 0) { | |
throw new Error("Number of shares must be positive for equal distribution."); | |
} | |
// Calculate tax amount | |
const taxAmount = this.multiply(Number(taxPercentage) / 100); | |
// Calculate net amount after tax | |
const netAmount = this.subtract(taxAmount); | |
// Distribute the net amount equally among shares | |
const netShares = netAmount.distributeEqually(numberOfShares); | |
// Add the tax amount to the first share | |
const allocatedShares = netShares.map((share, index) => | |
index === 0 ? share.add(taxAmount) : share | |
); | |
// If a minimum is specified for the first share | |
if (minimum !== undefined) { | |
let minimumMoney: Money; | |
if (minimum instanceof Money) { | |
if (minimum.currency.code !== this.#currency.code) { | |
throw new Error(`Minimum amount currency (${minimum.currency.code}) must match distribution currency (${this.#currency.code}).`); | |
} | |
minimumMoney = minimum; | |
} else { | |
minimumMoney = new Money(minimum, this.#currency.code); | |
} | |
// Check if the first allocated share is less than the minimum | |
if (allocatedShares[0].lessThan(minimumMoney)) { | |
// The first share should be the minimum. | |
const firstShare = minimumMoney; | |
// The remaining amount is total amount minus the minimum first share | |
const remainingAmount = this.subtract(firstShare); | |
// Distribute the remaining amount among the rest of the shares | |
// Need numberOfShares - 1 for the remaining shares | |
if (numberOfShares > 1) { | |
const restShares = remainingAmount.distributeEqually(numberOfShares - 1); | |
return [firstShare, ...restShares]; | |
} else { | |
// If only one share was requested, and it's less than the minimum, | |
// the single share is simply the minimum. | |
return [firstShare]; | |
} | |
} | |
} | |
return allocatedShares; | |
} | |
/** | |
* Distributes the money equally with tax consideration, but only applies the minimum | |
* logic if the net amount (after tax) is below a certain threshold. This interpretation | |
* seems slightly different from the original method name, which suggests tax *above* minimum, | |
* but the original code logic implies minimum on the first share *after* tax is added back. | |
* Replicating the logic from the provided code which focuses on the minimum for the first share. | |
* @param taxPercentage The percentage of tax to apply (e.g., 10 for 10%). Can be decimal. | |
* @param numberOfShares The number of shares to distribute into. | |
* @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, or minimum is not provided/invalid. | |
*/ | |
distributeEquallyWithTaxAboveMinimum( | |
taxPercentage: number | string, | |
numberOfShares: number, | |
minimum: number | string | Money, // Minimum is required here based on original signature | |
): Money[] { | |
if (numberOfShares <= 0) { | |
throw new Error("Number of shares must be positive for equal distribution."); | |
} | |
if (minimum === undefined || minimum === null) { | |
throw new Error("Minimum amount must be provided for distributeEquallyWithTaxAboveMinimum."); | |
} | |
// Calculate tax amount | |
const taxAmount = this.multiply(Number(taxPercentage) / 100); | |
// Calculate net amount after tax | |
const netAmount = this.subtract(taxAmount); | |
// Convert minimum to Money instance | |
let minimumMoney: Money; | |
if (minimum instanceof Money) { | |
if (minimum.currency.code !== this.#currency.code) { | |
throw new Error(`Minimum amount currency (${minimum.currency.code}) must match distribution currency (${this.#currency.code}).`); | |
} | |
minimumMoney = minimum; | |
} else { | |
minimumMoney = new Money(minimum, this.#currency.code); | |
} | |
// Original code had a check `if (!net?.lessThanOrEqual(minimum)) { return this; }` | |
// This condition seems incorrect for the function name and intended logic (distributing net shares). | |
// It would return the original amount if the net is NOT less than or equal to the minimum, | |
// which doesn't align with the distribution goal. | |
// I will proceed with the distribution logic and apply the minimum check on the first share, | |
// similar to how it was done in the `distributeEquallyWithTax` method in the original file. | |
// If the intention was to *only* distribute if the net is above a minimum, the logic needs clarification. | |
// Assuming the goal is to distribute the net, add tax to the first, and ensure the first is at least `minimum`: | |
// Distribute the net amount equally among shares | |
const netShares = netAmount.distributeEqually(numberOfShares); | |
// Add the tax amount to the first share | |
const allocatedShares = netShares.map((share, index) => | |
index === 0 ? share.add(taxAmount) : share | |
); | |
// Check if the first allocated share is less than the minimum | |
if (allocatedShares[0].lessThan(minimumMoney)) { | |
// The first share should be the minimum. | |
const firstShare = minimumMoney; | |
// The remaining amount is total amount minus the minimum first share | |
const remainingAmount = this.subtract(firstShare); | |
// Distribute the remaining amount among the rest of the shares | |
// Need numberOfShares - 1 for the remaining shares | |
if (numberOfShares > 1) { | |
const restShares = remainingAmount.distributeEqually(numberOfShares - 1); | |
return [firstShare, ...restShares]; | |
} else { | |
// If only one share was requested, and it's less than the minimum, | |
// the single share is simply the minimum. | |
return [firstShare]; | |
} | |
} | |
return allocatedShares; | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
```typescript | |
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) | |
const amount1 = new Money(123.45, 'USD'); | |
console.log(`Created amount1: ${amount1.toDecimal()} ${amount1.currency.code}`); | |
// Expected: Created amount1: 123.45 USD | |
// Create from a string (major units) | |
const amount2 = new Money('99.99', 'EUR'); | |
console.log(`Created amount2: ${amount2.toDecimal()} ${amount2.currency.code}`); | |
// Expected: Created amount2: 99.99 EUR | |
// Create from a number (minor units) - use isMinorUnit: true | |
const amount3 = new Money(2500n, 'ZMW', true); // 2500 ngwee = 25 Kwacha | |
console.log(`Created amount3 (minor units): ${amount3.toDecimal()} ${amount3.currency.code}`); | |
// Expected: Created amount3 (minor units): 25.00 ZMW | |
// Create from another Money instance | |
const amount4 = new Money(amount1); | |
console.log(`Created amount4 from amount1: ${amount4.toDecimal()} ${amount4.currency.code}`); | |
// Expected: Created amount4 from amount1: 123.45 USD | |
// Using static helper for major units | |
const amount5 = Money.fromMajor(50.75, 'USD'); | |
console.log(`Created amount5 from major: ${amount5.toDecimal()} ${amount5.currency.code}`); | |
// Expected: Created amount5 from major: 50.75 USD | |
// 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}`); | |
// Expected: Created amount6 from minor: 100.00 ZMW | |
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'); | |
// Addition | |
const sum = usd1.add(usd2); | |
console.log(`${usd1.toDecimal()} + ${usd2.toDecimal()} = ${sum.toDecimal()} ${sum.currency.code}`); | |
// Expected: 100.50 + 50.25 = 150.75 USD | |
// Subtract | |
const difference = usd1.subtract(usd2); | |
console.log(`${usd1.toDecimal()} - ${usd2.toDecimal()} = ${difference.toDecimal()} ${difference.currency.code}`); | |
// Expected: 100.50 - 50.25 = 50.25 USD | |
// Multiply by a number | |
const product = usd1.multiply(2.5); | |
console.log(`${usd1.toDecimal()} * 2.5 = ${product.toDecimal()} ${product.currency.code}`); | |
// Expected: 100.50 * 2.5 = 251.25 USD | |
// Divide by a number | |
const quotient = usd1.divide(4); | |
console.log(`${usd1.toDecimal()} / 4 = ${quotient.toDecimal()} ${quotient.currency.code}`); | |
// Expected: 100.50 / 4 = 25.12 USD (Note: BigInt integer division truncates, adjust if specific rounding needed) | |
// Add percentage | |
const amountWithTax = usd1.addPercentage(10); // Add 10% | |
console.log(`${usd1.toDecimal()} + 10% = ${amountWithTax.toDecimal()} ${amountWithTax.currency.code}`); | |
// Expected: 100.50 + 10% = 110.55 USD | |
// Subtract percentage | |
const amountAfterDiscount = usd1.subtractPercentage(5); // Subtract 5% | |
console.log(`${usd1.toDecimal()} - 5% = ${amountAfterDiscount.toDecimal()} ${amountAfterDiscount.currency.code}`); | |
// Expected: 100.50 - 5% = 95.47 USD | |
console.log('\n'); | |
// 3. Formatting | |
console.log('--- Formatting ---'); | |
const zmw = new Money(1500.75, 'ZMW'); | |
// Default locale (en-ZM from user preferences) | |
console.log(`Formatted ${zmw.toDecimal()} (default): ${zmw.format()}`); | |
// Expected: Formatted 1500.75 (default): K1,500.75 | |
// US Dollar formatting | |
const usdAmount = new Money(2500.99, 'USD'); | |
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'); // Note: Input will be rounded to 2 decimal places due to EUR exponent | |
console.log(`Formatted ${eurAmount.toDecimal()} (de-DE, min/max 2 digits): ${eurAmount.format('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`); | |
// Expected: Formatted 1234.57 (de-DE, min/max 2 digits): 1.234,57 € | |
console.log('\n'); | |
// 4. Comparisons | |
console.log('--- Comparisons ---'); | |
const comp1 = new Money(50, 'USD'); | |
const comp2 = new Money(100, 'USD'); | |
const comp3 = new Money(50, 'USD'); | |
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 (Note: equal checks currency too) | |
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 | |
// 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').isZero()}? ${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'); | |
// Allocate by ratios [1, 2, 3] | |
const allocated = totalAmount.allocate([1, 2, 3]); | |
console.log(`Allocating ${totalAmount.toDecimal()} with ratios [1, 2, 3]:`); | |
allocated.forEach(share => console.log(`- ${share.toDecimal()} ${share.currency.code}`)); | |
// Expected: | |
// Allocating 100.00 with ratios [1, 2, 3]: | |
// - 16.67 USD (Note: May vary slightly due to BigInt division and remainder distribution) | |
// - 33.33 USD | |
// - 50.00 USD | |
// Distribute equally into 4 shares | |
const equallyDistributed = totalAmount.distributeEqually(4); | |
console.log(`Distributing ${totalAmount.toDecimal()} equally into 4 shares:`); | |
equallyDistributed.forEach(share => console.log(`- ${share.toDecimal()} ${share.currency.code}`)); | |
// Expected: | |
// Distributing 100.00 equally into 4 shares: | |
// - 25.00 USD | |
// - 25.00 USD | |
// - 25.00 USD | |
// - 25.00 USD | |
// Distribute equally with tax (10%) and minimum (30) for the first share | |
const taxDistribute = new Money(120, 'USD'); | |
const sharesWithTaxAndMin = taxDistribute.distributeEquallyWithTax(10, 3, 30); // 10% tax, 3 shares, min 30 | |
console.log(`Distributing ${taxDistribute.toDecimal()} with 10% tax and min 30 for first share (3 shares):`); | |
sharesWithTaxAndMin.forEach(share => console.log(`- ${share.toDecimal()} ${share.currency.code}`)); | |
// Expected: | |
// Distributing 120.00 with 10% tax and min 30 for first share (3 shares): | |
// Tax = 120 * 0.10 = 12.00 | |
// Net = 120 - 12 = 108.00 | |
// Net shares = 108 / 3 = 36.00 each | |
// First share gets tax back: 36 + 12 = 48.00 | |
// 48.00 is not less than min 30, so distribution is [48.00, 36.00, 36.00] | |
// - 48.00 USD | |
// - 36.00 USD | |
// - 36.00 USD | |
// Example where minimum is applied | |
const taxDistributeMinApplied = new Money(100, 'USD'); | |
const sharesWithTaxAndMinApplied = taxDistributeMinApplied.distributeEquallyWithTax(10, 3, 40); // 10% tax, 3 shares, min 40 | |
console.log(`Distributing ${taxDistributeMinApplied.toDecimal()} with 10% tax and min 40 for first share (3 shares):`); | |
sharesWithTaxAndMinApplied.forEach(share => console.log(`- ${share.toDecimal()} ${share.currency.code}`)); | |
// Expected: | |
// Distributing 100.00 with 10% tax and min 40 for first share (3 shares): | |
// Tax = 100 * 0.10 = 10.00 | |
// Net = 100 - 10 = 90.00 | |
// Net shares = 90 / 3 = 30.00 each | |
// First share gets tax back: 30 + 10 = 40.00 | |
// 40.00 is not less than min 40. Hmm, the original logic description or implementation might need review. | |
// Let's follow the provided code's logic again: | |
// Original code for `distributeEquallyWithTax`: | |
// If allocated[0]?.lessThan(minimum), then first = new Money(minimum), rest = original.subtract(first).distributeEqually(numberOfShares - 1) | |
// This means if the first calculated share (net share + tax) is less than the minimum, the first share becomes the minimum, and the *remaining* amount (total - minimum) is distributed among the rest. | |
// Recalculating based on original code's logic: | |
// Total = 100, Tax% = 10, Shares = 3, Min = 40 | |
// Tax = 10 | |
// Net = 90 | |
// Net shares = [30, 30, 30] | |
// Allocated shares (tax on first) = [40, 30, 30] -> Oh, wait, my previous manual calc added tax before comparing to min. The original code adds tax, *then* checks against minimum. | |
// Allocated shares (tax on first) = [30 + 10, 30, 30] = [40, 30, 30] | |
// Is allocated[0] (40) < minimum (40)? No, it's equal. So the minimum logic is NOT triggered. | |
// The expected output is [40.00, 30.00, 30.00]. | |
// Let's try an example where min is applied. Min = 45 | |
const sharesWithTaxAndMinTriggered = taxDistributeMinApplied.distributeEquallyWithTax(10, 3, 45); // 10% tax, 3 shares, min 45 | |
console.log(`Distributing ${taxDistributeMinApplied.toDecimal()} with 10% tax and min 45 for first share (3 shares):`); | |
sharesWithTaxAndMinTriggered.forEach(share => console.log(`- ${share.toDecimal()} ${share.currency.code}`)); | |
// Expected: | |
// Total = 100, Tax% = 10, Shares = 3, Min = 45 | |
// Tax = 10 | |
// Net = 90 | |
// Net shares = [30, 30, 30] | |
// Allocated shares (tax on first) = [40, 30, 30] | |
// Is allocated[0] (40) < minimum (45)? Yes. | |
// First share = minimum (45). | |
// Remaining amount = Total (100) - First share (45) = 55.00 | |
// Distribute remaining (55.00) among remaining shares (3-1=2 shares). | |
// 55.00 / 2 = 27.50 each. | |
// Result: [45.00, 27.50, 27.50] | |
// - 45.00 USD | |
// - 27.50 USD | |
// - 27.50 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 | |
try { | |
const usdToConvert = new Money(100, 'USD'); | |
const convertedEur = usdToConvert.convert('EUR', usdToEurRate); | |
console.log(`${usdToConvert.toDecimal()} ${usdToConvert.currency.code} converted to EUR: ${convertedEur.toDecimal()} ${convertedEur.currency.code}`); | |
// Expected: 100.00 USD converted to EUR: 92.00 EUR (100 * 0.92) | |
} 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'); // Note: Stored as 12346n due to USD exponent 2 and rounding in majorToMinor | |
console.log(`Original number: 123.4567`); | |
console.log(`Stored amount (minor units): ${preciseAmount.amount}`); // BigInt value | |
// Expected: Stored amount (minor units): 12346 | |
console.log(`toDecimal(): ${preciseAmount.toDecimal()}`); | |
// Expected: toDecimal(): 123.46 | |
console.log(`toNumber(): ${preciseAmount.toNumber()}`); | |
// Expected: toNumber(): 123.46 (Note: conversion to number might lose precision for display if the original number had more decimal places than the currency exponent) | |
console.log('\n'); | |
// 8. Minimum and Maximum | |
console.log('--- Minimum and Maximum ---'); | |
const minMax1 = new Money(20, 'USD'); | |
const minMax2 = new Money(50, 'USD'); | |
const minMax3 = new Money(10, 'USD'); | |
const minimumAmount = minMax1.minimum(minMax2, minMax3); | |
console.log(`Minimum of ${minMax1.toDecimal()}, ${minMax2.toDecimal()}, ${minMax3.toDecimal()}: ${minimumAmount.toDecimal()} ${minimumAmount.currency.code}`); | |
// Expected: Minimum of 20.00, 50.00, 10.00: 10.00 USD | |
const maximumAmount = minMax1.maximum(minMax2, minMax3); | |
console.log(`Maximum of ${minMax1.toDecimal()}, ${minMax2.toDecimal()}, ${minMax3.toDecimal()}: ${maximumAmount.toDecimal()} ${maximumAmount.currency.code}`); | |
// Expected: Maximum of 20.00, 50.00, 10.00: 50.00 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()} ${minMaxWithNumber.currency.code}`); | |
// Expected: Minimum of 20.00, 5.00, 15.00: 5.00 USD | |
console.log('\n'); | |
// 9. Same Amount / Same Currency | |
console.log('--- Same Amount / Same Currency ---'); | |
const same1 = new Money(100, 'USD'); | |
const same2 = new Money(100, 'USD'); | |
const same3 = new Money(100, 'EUR'); | |
const same4 = new Money(50, 'USD'); | |
console.log(`${same1.toDecimal()} and ${same2.toDecimal()} have same amount? ${same1.haveSameAmount(same2)}`); | |
// Expected: 100.00 and 100.00 have same amount? true | |
console.log(`${same1.toDecimal()} and ${same3.toDecimal()} have same amount? ${same1.haveSameAmount(same3)}`); | |
// Expected: 100.00 and 100.00 have same amount? true (Compares decimal values) | |
console.log(`${same1.toDecimal()} and ${same4.toDecimal()} have same amount? ${same1.haveSameAmount(same4)}`); | |
// Expected: 100.00 and 50.00 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 | |
const fractionalAmount = new Money(100.50, 'USD'); // Stored as 10050n | |
const exactFractionalAmount = new Money(100.00, 'USD'); // Stored as 10000n | |
console.log(`${wholeAmount.toDecimal()} has subunits? ${wholeAmount.hasSubUnits()}`); | |
// Expected: 100.00 has subunits? false (Remainder when divided by 10^2 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('\n'); | |
// 11. Formula Evaluation (Using mathjs - see notes about precision) | |
console.log('--- Formula Evaluation ---'); | |
// Requires 'mathjs' to be installed (`npm install mathjs`) | |
// 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. | |
// 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'); // Exponent 2 (cents) | |
console.log(`Original amount: ${originalAmount.toDecimal()} (Exponent ${originalAmount.currency.exponent})`); | |
// Expected: Original amount: 123.45 (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()} (Exponent ${transformedToExponent0.currency.exponent})`); | |
console.log(`Internal amount BigInt: ${transformedToExponent0.amount}`); | |
// Expected: Transformed to exponent 0: 123.00 (Exponent 0) -> Note: This loses fractional part due to integer division when scaling down. | |
// 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()} (Exponent ${transformedBackToExponent2.currency.exponent})`); | |
console.log(`Internal amount BigInt: ${transformedBackToExponent2.amount}`); | |
// Expected: Transformed back to exponent 2: 123.00 (Exponent 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()} (Exponent ${transformedToExponent3.currency.exponent})`); | |
console.log(`Internal amount BigInt: ${transformedToExponent3.amount}`); | |
// Expected: Transformed to exponent 3: 123.450 (Exponent 3) | |
// Expected: Internal amount BigInt: 123450n (123.45 USD * 10^1 = 1234.5, but scaled by 10^3 from original 10^2 is 10^1 factor) | |
// Let's recheck the transformScale logic: current exponent 2, target 3. Diff = 1. Scale up amount by 10^1. | |
// Original amount BigInt for 123.45 is 12345n (at exponent 2). | |
// Scaled amount for exponent 3: 12345n * 10^1 = 123450n. New currency has exponent 3. | |
// Result: 123450n at exponent 3 = 123.450. This seems correct. | |
// Expected: Transformed to exponent 3: 123.450 (Exponent 3) | |
// Expected: Internal amount BigInt: 123450n | |
``` | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment