Skip to content

Instantly share code, notes, and snippets.

@stctheproducer
Last active May 23, 2025 15:21
Show Gist options
  • Save stctheproducer/3ca2c0622896585ba1abe08a3747164e to your computer and use it in GitHub Desktop.
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 {
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;
}
}
```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