import { Money } from './money'; // Assuming the Money class is in money.ts
// --- Usage Examples ---
// 1. Creating Money instances
console.log('--- Creating Money Instances ---');
// Create from a number (major units). Currency exponent is 2.
const amount1 = new Money(123.45, 'USD');
console.log(`Created amount1: ${amount1.toDecimal()} ${amount1.currency.code} (scale: ${amount1.scale})`);
// Expected: Created amount1: 123.45 USD (scale: 2)
// Create from a string (major units). Currency exponent is 2.
const amount2 = new Money('99.99', 'EUR');
console.log(`Created amount2: ${amount2.toDecimal()} ${amount2.currency.code} (scale: ${amount2.scale})`);
// Expected: Created amount2: 99.99 EUR (scale: 2)
// Create from a number (minor units) - use isMinorUnit: true
// ZMW has exponent 2. No explicit scale provided, so it defaults to currency exponent.
const amount3 = new Money(2500n, 'ZMW', true); // 2500 ngwee = 25 Kwacha
console.log(`Created amount3 (minor units): ${amount3.toDecimal()} ${amount3.currency.code} (scale: ${amount3.scale})`);
// Expected: Created amount3 (minor units): 25.00 ZMW (scale: 2)
// Create from another Money instance
const amount4 = new Money(amount1);
console.log(`Created amount4 from amount1: ${amount4.toDecimal()} ${amount4.currency.code} (scale: ${amount4.scale})`);
// Expected: Created amount4 from amount1: 123.45 USD (scale: 2)
// Using static helper for major units
const amount5 = Money.fromMajor(50.75, 'USD');
console.log(`Created amount5 from major: ${amount5.toDecimal()} ${amount5.currency.code} (scale: ${amount5.scale})`);
// Expected: Created amount5 from major: 50.75 USD (scale: 2)
// Using static helper for minor units
const amount6 = Money.fromMinor(10000n, 'ZMW'); // 10000 ngwee = 100 Kwacha
console.log(`Created amount6 from minor: ${amount6.toDecimal()} ${amount6.currency.code} (scale: ${amount6.scale})`);
// Expected: Created amount6 from minor: 100.00 ZMW (scale: 2)
// --- New examples demonstrating automatic scale detection and higher precision ---
// Input with more decimal places than currency exponent
const preciseAmount1 = new Money(1.23456, 'USD'); // USD exponent is 2, input has 5 decimals
console.log(`Created preciseAmount1: ${preciseAmount1.toDecimal()} ${preciseAmount1.currency.code} (scale: ${preciseAmount1.scale})`);
// Expected: Created preciseAmount1: 1.23456 USD (scale: 5)
// Input with fewer decimal places than currency exponent, scale defaults to currency exponent
const preciseAmount2 = new Money(12.5, 'ZMW'); // ZMW exponent is 2, input has 1 decimal
console.log(`Created preciseAmount2: ${preciseAmount2.toDecimal()} ${preciseAmount2.currency.code} (scale: ${preciseAmount2.scale})`);
// Expected: Created preciseAmount2: 12.50 ZMW (scale: 2)
// Example requested from prompt: 1.23456 should have minor amount 123456, scale 5
const num1 = new Money('1.23456', 'USD');
console.log(`num1: ${num1.toDecimal()} (amount: ${num1.amount}, scale: ${num1.scale})`);
// Expected: num1: 1.23456 (amount: 123456n, scale: 5)
// Example requested from prompt: 12.5 should have minor amount 1250, scale 2
const num2 = new Money('12.5', 'ZMW');
console.log(`num2: ${num2.toDecimal()} (amount: ${num2.amount}, scale: ${num2.scale})`);
// Expected: num2: 12.50 (amount: 1250n, scale: 2)
console.log('\n');
// 2. Basic Arithmetic Operations (Assumes same currency)
console.log('--- Arithmetic Operations ---');
const usd1 = new Money(100.50, 'USD');
const usd2 = new Money(50.25, 'USD');
const usdHighPrecision = new Money(10.1234, 'USD'); // Scale 4
const multiplierWithDecimals = 2.5; // Scale 1 for the multiplier value itself
const divisorWithDecimals = 4; // Treated as 4.0, scale 0
// Addition
const sum = usd1.add(usd2);
console.log(`${usd1.toDecimal()} (s:${usd1.scale}) + ${usd2.toDecimal()} (s:${usd2.scale}) = ${sum.toDecimal()} (s:${sum.scale}) ${sum.currency.code}`);
// Expected: 100.50 (s:2) + 50.25 (s:2) = 150.75 (s:2) USD
// Addition with different scales
const sumHighPrecision = usd1.add(usdHighPrecision); // Common scale will be 4
console.log(`${usd1.toDecimal()} (s:${usd1.scale}) + ${usdHighPrecision.toDecimal()} (s:${usdHighPrecision.scale}) = ${sumHighPrecision.toDecimal()} (s:${sumHighPrecision.scale}) ${sumHighPrecision.currency.code}`);
// Expected: 100.50 (s:2) + 10.1234 (s:4) = 110.6234 (s:4) USD
// Subtract
const difference = usd1.subtract(usd2);
console.log(`${usd1.toDecimal()} (s:${usd1.scale}) - ${usd2.toDecimal()} (s:${usd2.scale}) = ${difference.toDecimal()} (s:${difference.scale}) ${difference.currency.code}`);
// Expected: 100.50 (s:2) - 50.25 (s:2) = 50.25 (s:2) USD
// Subtract with different scales
const differenceHighPrecision = usdHighPrecision.subtract(usd1); // Common scale will be 4
console.log(`${usdHighPrecision.toDecimal()} (s:${usdHighPrecision.scale}) - ${usd1.toDecimal()} (s:${usd1.scale}) = ${differenceHighPrecision.toDecimal()} (s:${differenceHighPrecision.scale}) ${differenceHighPrecision.currency.code}`);
// Expected: 10.1234 (s:4) - 100.50 (s:2) = -90.3766 (s:4) USD
// Multiply by a number
const product = usd1.multiply(multiplierWithDecimals); // usd1 scale 2, multiplier scale 1. Result scale 2+1=3
console.log(`${usd1.toDecimal()} (s:${usd1.scale}) * ${multiplierWithDecimals} (s:1) = ${product.toDecimal()} (s:${product.scale}) ${product.currency.code}`);
// Expected: 100.50 (s:2) * 2.5 (s:1) = 251.250 (s:3) USD
// Divide by a number
const quotient = usd1.divide(divisorWithDecimals); // usd1 scale 2, divisor scale 0. Result scale 2 + BUFFER (e.g., 6) = 8
console.log(`${usd1.toDecimal()} (s:${usd1.scale}) / ${divisorWithDecimals} (s:0) = ${quotient.toDecimal()} (s:${quotient.scale}) ${quotient.currency.code}`);
// Expected: 100.50 (s:2) / 4 (s:0) = 25.12500000 (s:8) USD (or similar, depending on DIVISION_PRECISION_BUFFER)
// Add percentage
const amountWithTax = usd1.addPercentage(10); // 100.50 * 0.10 = 10.05. 100.50 + 10.05 = 110.55
console.log(`${usd1.toDecimal()} + 10% = ${amountWithTax.toDecimal()} (s:${amountWithTax.scale}) ${amountWithTax.currency.code}`);
// Expected: 100.50 + 10% = 110.550 (s:3) USD (because 10% translates to multiplier 0.10 which has scale 2)
// Subtract percentage
const amountAfterDiscount = usd1.subtractPercentage(5); // 100.50 * 0.05 = 5.025. 100.50 - 5.025 = 95.475
console.log(`${usd1.toDecimal()} - 5% = ${amountAfterDiscount.toDecimal()} (s:${amountAfterDiscount.scale}) ${amountAfterDiscount.currency.code}`);
// Expected: 100.50 - 5% = 95.475 (s:3) USD
console.log('\n');
// 3. Formatting
console.log('--- Formatting ---');
const zmw = new Money(1500.75, 'ZMW'); // Scale 2
const highPrecisionZMW = new Money(1500.12345, 'ZMW'); // Scale 5
// Default locale (en-ZM from user preferences), uses instance's scale
console.log(`Formatted ${zmw.toDecimal()} (default): ${zmw.format()}`);
// Expected: Formatted 1500.75 (default): K1,500.75
// Formatting with high precision, uses instance's scale
console.log(`Formatted ${highPrecisionZMW.toDecimal()} (default, high precision): ${highPrecisionZMW.format()}`);
// Expected: Formatted 1500.12345 (default, high precision): K1,500.12345
// US Dollar formatting
const usdAmount = new Money(2500.99, 'USD'); // Scale 2
console.log(`Formatted ${usdAmount.toDecimal()} (en-US): ${usdAmount.format('en-US')}`);
// Expected: Formatted 2500.99 (en-US): $2,500.99
// Euro formatting with minimumFractionDigits option
const eurAmount = new Money(1234.567, 'EUR'); // EUR exponent 2. Input 3 decimals, so scale will be 3.
console.log(`Formatted ${eurAmount.toDecimal()} (de-DE, min/max 2 digits): ${eurAmount.format('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
// Expected: Formatted 1234.567 (de-DE, min/max 2 digits): 1.234,57 € (because format options override instance scale)
console.log('\n');
// 4. Comparisons
console.log('--- Comparisons ---');
const comp1 = new Money(50, 'USD'); // Scale 2
const comp2 = new Money(100, 'USD'); // Scale 2
const comp3 = new Money(50, 'USD'); // Scale 2
const compDifferentScale = new Money('50.000', 'USD'); // Scale 3
const compDifferentCurrency = new Money(50, 'EUR'); // Scale 2
console.log(`${comp1.toDecimal()} < ${comp2.toDecimal()}? ${comp1.lessThan(comp2)}`);
// Expected: 50.00 < 100.00? true
console.log(`${comp1.toDecimal()} <= ${comp3.toDecimal()}? ${comp1.lessThanOrEqual(comp3)}`);
// Expected: 50.00 <= 50.00? true
console.log(`${comp1.toDecimal()} == ${comp3.toDecimal()}? ${comp1.equal(comp3)}`);
// Expected: 50.00 == 50.00? true (Currency must also be same)
console.log(`${comp1.toDecimal()} > ${comp2.toDecimal()}? ${comp1.greaterThan(comp2)}`);
// Expected: 50.00 > 100.00? false
console.log(`${comp2.toDecimal()} >= ${comp1.toDecimal()}? ${comp2.greaterThanOrEqual(comp1)}`);
// Expected: 100.00 >= 50.00? true
// Comparisons with different scales
console.log(`${comp1.toDecimal()} (s:${comp1.scale}) == ${compDifferentScale.toDecimal()} (s:${compDifferentScale.scale})? ${comp1.equal(compDifferentScale)}`);
// Expected: 50.00 (s:2) == 50.000 (s:3)? true (values are normalized internally)
// Comparison with numbers (treated as major units in the Money instance's currency)
console.log(`${comp1.toDecimal()} < 100? ${comp1.lessThan(100)}`);
// Expected: 50.00 < 100? true
console.log(`${comp1.toDecimal()} == 50.00? ${comp1.equal(50.00)}`);
// Expected: 50.00 == 50.00? true
// Checking signs
const negativeAmount = new Money(-25.50, 'USD');
console.log(`${negativeAmount.toDecimal()} is negative? ${negativeAmount.isNegative()}`);
// Expected: -25.50 is negative? true
console.log(`${new Money(0, 'USD').toDecimal()} is zero? ${new Money(0, 'USD').isZero()}`);
// Expected: 0.00 is zero? true
console.log(`${usd1.toDecimal()} is positive? ${usd1.isPositive()}`);
// Expected: 100.50 is positive? true
console.log(`Absolute of ${negativeAmount.toDecimal()}: ${negativeAmount.absolute().toDecimal()}`);
// Expected: Absolute of -25.50: 25.50
console.log('\n');
// 5. Allocation and Distribution
console.log('--- Allocation and Distribution ---');
const totalAmount = new Money(100, 'USD'); // Scale 2
const totalHighPrecisionAmount = new Money(100.123, 'USD'); // Scale 3
// Allocate by ratios [1, 2, 3]
const allocated = totalAmount.allocate([1, 2, 3]);
console.log(`Allocating ${totalAmount.toDecimal()} (s:${totalAmount.scale}) with ratios [1, 2, 3]:`);
allocated.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected:
// Allocating 100.00 (s:2) with ratios [1, 2, 3]:
// - 16.67 (s:2) USD
// - 33.33 (s:2) USD
// - 50.00 (s:2) USD
// Distribute equally into 4 shares
const equallyDistributed = totalHighPrecisionAmount.distributeEqually(4);
console.log(`Distributing ${totalHighPrecisionAmount.toDecimal()} (s:${totalHighPrecisionAmount.scale}) equally into 4 shares:`);
equallyDistributed.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected:
// Distributing 100.123 (s:3) equally into 4 shares:
// - 25.031 (s:3) USD
// - 25.031 (s:3) USD
// - 25.031 (s:3) USD
// - 25.030 (s:3) USD (remainder handling makes last share potentially different)
// --- distributeEquallyWithTax Examples (Exclusive vs. Inclusive Tax) ---
// Scenario 1: Exclusive Tax (default behavior)
// Total 120, 10% tax (exclusive) = 12.00 tax. Distribute 120. First share gets 12 back.
const exclusiveTaxDistribute = new Money(120, 'USD'); // Scale 2
const sharesExclusiveTax = exclusiveTaxDistribute.distributeEquallyWithTax(10, 3); // 10% tax, 3 shares, default exclusive
console.log(`\nDistributing ${exclusiveTaxDistribute.toDecimal()} with 10% EXCLUSIVE tax (3 shares):`);
sharesExclusiveTax.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected Calculation:
// Total = 120.00
// Tax = 120.00 * 0.10 = 12.00 (Calculated on Total)
// Shares BEFORE tax allocation (distribute original total) = 120.00 / 3 = 40.00 each
// First Share (gets tax added) = 40.00 + 12.00 = 52.00
// Final shares: [52.00, 40.00, 40.00]
// - 52.00 (s:2) USD
// - 40.00 (s:2) USD
// - 40.00 (s:2) USD
// Scenario 2: Exclusive Tax with Minimum Applied
const exclusiveTaxMinApplied = new Money(100, 'USD'); // Scale 2
const sharesExclusiveTaxMin = exclusiveTaxMinApplied.distributeEquallyWithTax(10, 3, false, 45); // 10% tax, 3 shares, exclusive, min 45
console.log(`\nDistributing ${exclusiveTaxMinApplied.toDecimal()} with 10% EXCLUSIVE tax and min 45 (3 shares):`);
sharesExclusiveTaxMin.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected Calculation:
// Total = 100.00
// Tax = 100.00 * 0.10 = 10.00 (Calculated on Total)
// Shares BEFORE tax allocation = 100.00 / 3 = 33.33 (approx)
// First Share (gets tax added) = 33.33 + 10.00 = 43.33
// Is 43.33 < minimum (45)? Yes. So minimum logic IS triggered.
// Enforced First Share = 45.00
// Remaining Amount = Total (100.00) - Enforced First Share (45.00) = 55.00
// Distribute Remaining (55.00) among remaining shares (3 - 1 = 2 shares).
// Rest Shares = 55.00 / 2 = 27.50 each
// Final shares: [45.00, 27.50, 27.50]
// - 45.00 (s:2) USD
// - 27.50 (s:2) USD
// - 27.50 (s:2) USD
// Scenario 3: Inclusive Tax
// Total 120, 10% tax (inclusive).
// Net = 120 / (1 + 0.10) = 120 / 1.10 = 109.0909...
// Tax Amount = 120 - 109.09 = 10.91 (approx)
// Distribute 109.09. First share gets 10.91 back.
const inclusiveTaxDistribute = new Money(120, 'USD'); // Scale 2
const sharesInclusiveTax = inclusiveTaxDistribute.distributeEquallyWithTax(10, 3, true); // 10% tax, 3 shares, inclusive
console.log(`\nDistributing ${inclusiveTaxDistribute.toDecimal()} with 10% INCLUSIVE tax (3 shares):`);
sharesInclusiveTax.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected Calculation:
// Total = 120.00
// Net Amount to Distribute = 120 / 1.10 = 109.09 (rounded for display)
// Tax Amount = 120 - 109.09 = 10.91 (rounded for display)
// Shares BEFORE tax allocation (distribute NET amount) = 109.09 / 3 = 36.36 (approx) each
// First Share (gets tax added) = 36.36 + 10.91 = 47.27 (approx)
// Final shares (due to rounding, amounts may vary slightly):
// - 47.27 (s:2) USD
// - 36.36 (s:2) USD
// - 36.36 (s:2) USD
// Scenario 4: Inclusive Tax with Minimum Applied
const inclusiveTaxMinApplied = new Money(110, 'USD'); // Scale 2
const sharesInclusiveTaxMin = inclusiveTaxMinApplied.distributeEquallyWithTax(10, 3, true, 40); // 10% tax, 3 shares, inclusive, min 40
console.log(`\nDistributing ${inclusiveTaxMinApplied.toDecimal()} with 10% INCLUSIVE tax and min 40 (3 shares):`);
sharesInclusiveTaxMin.forEach(share => console.log(`- ${share.toDecimal()} (s:${share.scale}) ${share.currency.code}`));
// Expected Calculation:
// Total = 110.00
// Net Amount to Distribute = 110 / 1.10 = 100.00
// Tax Amount = 110 - 100 = 10.00
// Shares BEFORE tax allocation = 100.00 / 3 = 33.33 (approx) each
// First Share (gets tax added) = 33.33 + 10.00 = 43.33
// Is 43.33 < minimum (40)? No.
// Final shares: [43.33, 33.33, 33.34] (or similar, due to rounding in 33.33 / 33.34 split)
// - 43.33 (s:2) USD
// - 33.33 (s:2) USD
// - 33.34 (s:2) USD
console.log('\n');
// 6. Currency Conversion (Basic implementation - requires external rates in a real app)
console.log('--- Currency Conversion ---');
// Note: This requires defining the target currency in the `currencies` registry
// and providing a manual conversion rate. A real-world scenario would fetch rates.
const usdToEurRate = 0.92; // Example: 1 USD = 0.92 EUR (scale of rate is 2)
try {
const usdToConvert = new Money(100, 'USD'); // Scale 2
const convertedEur = usdToConvert.convert('EUR', usdToEurRate);
console.log(`${usdToConvert.toDecimal()} (s:${usdToConvert.scale}) ${usdToConvert.currency.code} converted to EUR: ${convertedEur.toDecimal()} (s:${convertedEur.scale}) ${convertedEur.currency.code}`);
// Expected (100.00 USD * 0.92 EUR/USD = 92.00 EUR): 100.00 (s:2) USD converted to EUR: 92.00 (s:2) EUR
} catch (e: any) {
console.error(`Conversion failed: ${e.message}`);
}
try {
const usdToConvertHighPrec = new Money(100.123, 'USD'); // Scale 3
const eurRateHighPrec = '0.92123'; // Scale 5
const convertedEurHighPrec = usdToConvertHighPrec.convert('EUR', eurRateHighPrec);
console.log(`${usdToConvertHighPrec.toDecimal()} (s:${usdToConvertHighPrec.scale}) ${usdToConvertHighPrec.currency.code} converted to EUR: ${convertedEurHighPrec.toDecimal()} (s:${convertedEurHighPrec.scale}) ${convertedEurHighPrec.currency.code}`);
// Expected (100.123 * 0.92123 = 92.2346929 -> rounded to EUR exponent 2): 100.123 (s:3) USD converted to EUR: 92.23 (s:2) EUR
} catch (e: any) {
console.error(`Conversion failed: ${e.message}`);
}
console.log('\n');
// 7. toDecimal and toNumber
console.log('--- toDecimal and toNumber ---');
const preciseAmount = new Money(123.4567, 'USD'); // USD exponent 2. Input 4 decimals.
// Constructor sets #scale to 4. majorToMinor will result in 1234567n.
console.log(`Original number: 123.4567`);
console.log(`Stored amount (minor units): ${preciseAmount.amount}`);
// Expected: Stored amount (minor units): 1234567n
console.log(`Money instance scale: ${preciseAmount.scale}`);
// Expected: Money instance scale: 4
console.log(`toDecimal(): ${preciseAmount.toDecimal()}`);
// Expected: toDecimal(): 123.4567 (Uses instance's scale)
console.log(`toNumber(): ${preciseAmount.toNumber()}`);
// Expected: toNumber(): 123.4567 (Converts toDecimal() string to number)
console.log('\n');
// 8. Minimum and Maximum
console.log('--- Minimum and Maximum ---');
const minMax1 = new Money(20, 'USD'); // Scale 2
const minMax2 = new Money(50, 'USD'); // Scale 2
const minMax3 = new Money(10, 'USD'); // Scale 2
const minMax4 = new Money('10.000', 'USD'); // Scale 3
const minimumAmount = minMax1.minimum(minMax2, minMax3, minMax4);
console.log(`Minimum of ${minMax1.toDecimal()}, ${minMax2.toDecimal()}, ${minMax3.toDecimal()}, ${minMax4.toDecimal()}: ${minimumAmount.toDecimal()} (s:${minimumAmount.scale}) ${minimumAmount.currency.code}`);
// Expected: Minimum of 20.00, 50.00, 10.00, 10.000: 10.00 (s:2) USD (Returns the original object found to be minimum)
const maximumAmount = minMax1.maximum(minMax2, minMax3, minMax4);
console.log(`Maximum of ${minMax1.toDecimal()}, ${minMax2.toDecimal()}, ${minMax3.toDecimal()}, ${minMax4.toDecimal()}: ${maximumAmount.toDecimal()} (s:${maximumAmount.scale}) ${maximumAmount.currency.code}`);
// Expected: Maximum of 20.00, 50.00, 10.00, 10.000: 50.00 (s:2) USD
// Minimum/Maximum with numbers
const minMaxWithNumber = minMax1.minimum(5.00, 15.00);
console.log(`Minimum of ${minMax1.toDecimal()}, 5.00, 15.00: ${minMaxWithNumber.toDecimal()} (s:${minMaxWithNumber.scale}) ${minMaxWithNumber.currency.code}`);
// Expected: Minimum of 20.00, 5.00, 15.00: 5.00 (s:2) USD
console.log('\n');
// 9. Same Amount / Same Currency
console.log('--- Same Amount / Same Currency ---');
const same1 = new Money(100, 'USD'); // Scale 2
const same2 = new Money(100, 'USD'); // Scale 2
const same3 = new Money(100, 'EUR'); // Scale 2
const same4 = new Money(50, 'USD'); // Scale 2
const same5 = new Money('100.000', 'USD'); // Scale 3
console.log(`${same1.toDecimal()} (s:${same1.scale}) and ${same2.toDecimal()} (s:${same2.scale}) have same amount? ${same1.haveSameAmount(same2)}`);
// Expected: 100.00 (s:2) and 100.00 (s:2) have same amount? true
console.log(`${same1.toDecimal()} (s:${same1.scale}) and ${same3.toDecimal()} (s:${same3.scale}) have same amount? ${same1.haveSameAmount(same3)}`);
// Expected: 100.00 (s:2) and 100.00 (s:2) have same amount? true (Compares decimal values, ignores currency)
console.log(`${same1.toDecimal()} (s:${same1.scale}) and ${same5.toDecimal()} (s:${same5.scale}) have same amount? ${same1.haveSameAmount(same5)}`);
// Expected: 100.00 (s:2) and 100.000 (s:3) have same amount? true (Values are normalized for comparison)
console.log(`${same1.toDecimal()} (s:${same1.scale}) and ${same4.toDecimal()} (s:${same4.scale}) have same amount? ${same1.haveSameAmount(same4)}`);
// Expected: 100.00 (s:2) and 50.00 (s:2) have same amount? false
console.log(`${same1.toDecimal()} and ${same2.toDecimal()} have same currency? ${same1.haveSameCurrency(same2)}`);
// Expected: 100.00 and 100.00 have same currency? true
console.log(`${same1.toDecimal()} and ${same3.toDecimal()} have same currency? ${same1.haveSameCurrency(same3)}`);
// Expected: 100.00 and 100.00 have same currency? false
console.log('\n');
// 10. Has SubUnits
console.log('--- Has SubUnits ---');
const wholeAmount = new Money(100, 'USD'); // Stored as 10000n at scale 2
const fractionalAmount = new Money(100.50, 'USD'); // Stored as 10050n at scale 2
const exactFractionalAmount = new Money(100.00, 'USD'); // Stored as 10000n at scale 2
const ultraPreciseAmount = new Money(100.12345, 'USD'); // Stored as 10012345n at scale 5
console.log(`${wholeAmount.toDecimal()} has subunits? ${wholeAmount.hasSubUnits()}`);
// Expected: 100.00 has subunits? false (Remainder when divided by 10^2 (USD exponent) is 0)
console.log(`${fractionalAmount.toDecimal()} has subunits? ${fractionalAmount.hasSubUnits()}`);
// Expected: 100.50 has subunits? true (Remainder when divided by 10^2 is 50n)
console.log(`${exactFractionalAmount.toDecimal()} has subunits? ${exactFractionalAmount.hasSubUnits()}`);
// Expected: 100.00 has subunits? false (Remainder when divided by 10^2 is 0)
console.log(`${ultraPreciseAmount.toDecimal()} has subunits? ${ultraPreciseAmount.hasSubUnits()}`);
// Expected: 100.12345 has subunits? true (Remainder when divided by 10^2 is non-zero, e.g., 12345n % 100n != 0)
console.log('\n');
// 11. Formula Evaluation (Using mathjs - see notes about precision)
console.log('--- Formula Evaluation ---');
// Note: The `evaluateFormula` method in the generated code uses `mathjs`,
// which typically operates on numbers or its own BigNumber type, not necessarily the BigInt
// used internally by the Money class for storage. Precision might be limited by mathjs's capabilities.
// This example assumes `evaluateFormula` is uncommented in your Money class and `mathjs` is installed.
/*
// Example: Calculate (a + b) * c
const formulaResult = Money.evaluateFormula("(a + b) * c", { a: 10.50, b: 20.25, c: 2 }, 'USD');
console.log(`Formula "(a + b) * c" with a=10.50, b=20.25, c=2 = ${formulaResult.toDecimal()} ${formulaResult.currency.code}`);
// Expected: Formula "(a + b) * c" with a=10.50, b=20.25, c=2 = 61.50 USD
// Calculation: (10.50 + 20.25) * 2 = 30.75 * 2 = 61.50
// Example with slightly more complex numbers (mathjs might handle this)
const formulaResult2 = Money.evaluateFormula("x / y + z", { x: 100, y: 3, z: 10 }, 'USD');
console.log(`Formula "x / y + z" with x=100, y=3, z=10 = ${formulaResult2.toDecimal()} ${formulaResult2.currency.code}`);
// Expected: Formula "x / y + z" with x=100, y=3, z=10 = 43.33 USD (Mathjs will calculate 100/3 as ~33.333..., add 10, result ~43.333..., rounded to 2 decimals for USD)
// Example with invalid formula or variables
const invalidFormulaResult = Money.evaluateFormula("a + ", { a: 10 }, 'USD');
console.log(`Invalid formula "a + ": ${invalidFormulaResult.toDecimal()} ${invalidFormulaResult.currency.code}`);
// Expected: Invalid formula "a + ": 0.00 USD (Error is caught and returns 0)
*/
console.log('\n');
// 12. transformScale (Use with caution)
console.log('--- transformScale ---');
const originalAmount = new Money(123.45, 'USD'); // USD exponent 2. Input 2 decimals.
// Constructor creates: #amount = 12345n, #scale = 2.
console.log(`Original amount: ${originalAmount.toDecimal()} (current scale: ${originalAmount.scale}, currency exponent: ${originalAmount.currency.exponent})`);
// Expected: Original amount: 123.45 (current scale: 2, currency exponent: 2)
// Transform to exponent 0 (major units as the minor unit)
const transformedToExponent0 = originalAmount.transformScale(0);
console.log(`Transformed to exponent 0: ${transformedToExponent0.toDecimal()} (new scale: ${transformedToExponent0.scale})`);
console.log(`Internal amount BigInt: ${transformedToExponent0.amount}`);
// Expected (123.45 rounded to 0 decimal places is 123): Transformed to exponent 0: 123 (new scale: 0)
// Expected: Internal amount BigInt: 123n
// Transform back to exponent 2 from exponent 0 (will not recover lost precision)
const transformedBackToExponent2 = transformedToExponent0.transformScale(2);
console.log(`Transformed back to exponent 2: ${transformedBackToExponent2.toDecimal()} (new scale: ${transformedBackToExponent2.scale})`);
console.log(`Internal amount BigInt: ${transformedBackToExponent2.amount}`);
// Expected: Transformed back to exponent 2: 123.00 (new scale: 2)
// Expected: Internal amount BigInt: 12300n
// Transform to exponent 3 (milli-dollars) - scales up
const transformedToExponent3 = originalAmount.transformScale(3);
console.log(`Transformed to exponent 3: ${transformedToExponent3.toDecimal()} (new scale: ${transformedToExponent3.scale})`);
console.log(`Internal amount BigInt: ${transformedToExponent3.amount}`);
// Expected: Transformed to exponent 3: 123.450 (new scale: 3)
// Expected: Internal amount BigInt: 123450n
// Transform to exponent 1 for an amount with higher initial precision, demonstrating rounding
const preciseInitial = new Money(1.23456, 'USD'); // #amount = 123456n, #scale = 5
console.log(`\nOriginal precise: ${preciseInitial.toDecimal()} (current scale: ${preciseInitial.scale})`);
// Expected: Original precise: 1.23456 (current scale: 5)
const transformedToExponent1 = preciseInitial.transformScale(1); // Scale down from 5 to 1
console.log(`Transformed to exponent 1: ${transformedToExponent1.toDecimal()} (new scale: ${transformedToExponent1.scale})`);
console.log(`Internal amount BigInt: ${transformedToExponent1.amount}`);
// Expected (1.23456 rounded to 1 decimal place is 1.2): Transformed to exponent 1: 1.2 (new scale: 1)
// Expected: Internal amount BigInt: 12n (representing 1.2)
Last active
September 2, 2025 14:36
-
-
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<T = number> { | |
| code: string | |
| base: T | readonly T[] | |
| exponent: number // Number of decimal places in the minor unit | |
| } | |
| // A simple registry of supported currencies | |
| const currencies: { [key: string]: Currency<number> } = { | |
| ZMW: { code: 'ZMW', base: 10, exponent: 2 }, // Zambian Kwacha | |
| USD: { code: 'USD', base: 10, exponent: 2 }, // US Dollar | |
| EUR: { code: 'EUR', base: 10, exponent: 2 }, // Euro | |
| GBP: { code: 'GBP', base: 10, exponent: 2 }, // British Pound | |
| ZAR: { code: 'ZAR', base: 10, exponent: 2 }, // South African Rand | |
| // Add other currencies as needed or get them from the `@dinero.js/currencies@alpha` NPM package | |
| } | |
| /** | |
| * Converts a major unit number/string to a BigInt minor unit based on a target scale. | |
| * It handles padding with zeros if the input has fewer decimal places than targetScale, | |
| * and performs "round half up" rounding if the input has more decimal places. | |
| * | |
| * @param amount Major unit amount (e.g., 12.34 or '1.23456') | |
| * @param targetScale The desired number of decimal places for the minor unit. | |
| * @returns Amount in minor units as BigInt, correctly rounded. | |
| */ | |
| function majorToMinor(amount: number | string, targetScale: number): bigint { | |
| const amountStr = String(amount) | |
| const sign = amountStr.startsWith('-') ? -1n : 1n | |
| const absoluteAmountStr = amountStr.replace('-', '') // Work with absolute value for rounding | |
| const [integerPart, decimalPart] = absoluteAmountStr.split('.') | |
| const currentDecimalPlaces = decimalPart ? decimalPart.length : 0 | |
| // Convert the integer and decimal parts into a single BigInt without a decimal point. | |
| // This BigInt initially represents the number scaled by its *original* decimal places. | |
| const rawBigIntFromAbsolute = BigInt(`${integerPart}${decimalPart || ''}`) | |
| if (currentDecimalPlaces <= targetScale) { | |
| // If the input has fewer or equal decimal places than targetScale, | |
| // just pad with zeros to reach the targetScale. No rounding needed here. | |
| const scaleDifference = targetScale - currentDecimalPlaces | |
| return rawBigIntFromAbsolute * BigInt(10) ** BigInt(scaleDifference) * sign | |
| } else { | |
| // Input has more decimal places than targetScale. Need to perform rounding. | |
| const excessDecimalPlaces = currentDecimalPlaces - targetScale | |
| const powerOfTenExcess = BigInt(10) ** BigInt(excessDecimalPlaces) | |
| // Round half up: add half of the divisor before performing integer division. | |
| // For example, to round to 2 decimal places, if we have a number scaled by 3 decimal places (e.g., 1235n for 1.235), | |
| // we need to divide by 10. Adding 5 (half of 10) before division will round correctly. | |
| const roundingThreshold = powerOfTenExcess / 2n | |
| // Apply rounding before scaling down to the targetScale. | |
| // The addition of `roundingThreshold` effectively implements "round half up" | |
| // (rounding away from zero for values ending in .5). | |
| const roundedBigInt = (rawBigIntFromAbsolute + roundingThreshold) / powerOfTenExcess | |
| return roundedBigInt * sign // Apply the original sign | |
| } | |
| } | |
| /** | |
| * Creates a number formatter based on currency and locale. | |
| * @param currency The currency object. | |
| * @param locale The locale string (e.g., 'en-ZM'). | |
| * @param options Intl.NumberFormatOptions. | |
| * @returns Intl.NumberFormat instance. | |
| */ | |
| function getFormatter( | |
| currency: Currency, | |
| locale: string, | |
| options: Intl.NumberFormatOptions | |
| ): Intl.NumberFormat { | |
| return new Intl.NumberFormat(locale, { | |
| style: 'currency', | |
| currency: currency.code, | |
| minimumFractionDigits: currency.exponent, | |
| maximumFractionDigits: currency.exponent, | |
| ...options, | |
| }) | |
| } | |
| export class Money { | |
| #amount: bigint // Amount in the smallest currency unit (e.g., cents) | |
| #currency: Currency | |
| #scale: number | |
| /** | |
| * Creates a new Money instance. | |
| * @param amount The amount in major units (e.g., 12.34) or a Money instance, or a BigInt minor unit as string. | |
| * @param currencyCode The currency code (default: 'ZMW'). | |
| * @param isMinorUnit If true, the amount is treated as a minor unit BigInt string. | |
| * @param explicitScale Optional: When isMinorUnit is true, this explicitly sets the scale. | |
| * Otherwise, the currency's exponent is used as the minor unit scale. | |
| */ | |
| constructor( | |
| amount: number | string | Money, | |
| currencyCode: string | undefined = undefined, | |
| isMinorUnit: boolean = false, | |
| explicitScale: number | undefined = undefined | |
| ) { | |
| let finalCurrencyCode: string | |
| // Determine the final currency code | |
| if (amount instanceof Money) { | |
| // If 'amount' is already a Money instance | |
| if (currencyCode === undefined) { | |
| // If no currencyCode is provided, infer it from the Money instance | |
| finalCurrencyCode = amount.currency.code | |
| } else if (currencyCode !== amount.currency.code) { | |
| // If a currencyCode is provided and it's different from the Money instance's currency, | |
| // this is generally an error (or implies a conversion, which should be explicit). | |
| // For simplicity, we'll throw an error unless explicit conversion is intended. | |
| throw new Error( | |
| `Cannot create Money from instance with different currency (${amount.currency.code}) than target (${currencyCode}) without explicit conversion.` | |
| ) | |
| } else { | |
| finalCurrencyCode = currencyCode | |
| } | |
| } else { | |
| if (currencyCode === undefined) { | |
| // If 'amount' is a number or string, currencyCode defaults to 'ZMW' | |
| finalCurrencyCode = 'ZMW' | |
| } else { | |
| finalCurrencyCode = currencyCode | |
| } | |
| } | |
| // Validate and set the currency | |
| const currency = currencies[finalCurrencyCode] | |
| if (!currency) { | |
| throw new Error(`Unsupported currency code: ${finalCurrencyCode}`) | |
| } | |
| this.#currency = currency | |
| if (amount instanceof Money) { | |
| if (amount.currency.code !== this.#currency.code) { | |
| throw new Error( | |
| `Cannot create Money from instance with different currency (${amount.currency.code}) than target (${this.#currency.code}) without explicit conversion.` | |
| ) | |
| } | |
| this.#amount = amount.amount | |
| this.#scale = amount.scale // Preserve the source Money instance's scale | |
| } else { | |
| if (isMinorUnit) { | |
| // Treat the number/string directly as minor units (BigInt) | |
| this.#amount = BigInt(amount) | |
| // If an explicit scale is provided, use it, otherwise fall back to currency's exponent | |
| this.#scale = explicitScale !== undefined ? explicitScale : this.#currency.exponent | |
| } else { | |
| // Convert major unit number/string to minor unit BigInt | |
| const amountStr = String(amount) | |
| const [, decimalPart] = amountStr.split('.') | |
| // Determine the scale: use input's decimal places, but ensure it's at least the currency's exponent | |
| this.#scale = decimalPart | |
| ? Math.max(decimalPart.length, this.#currency.exponent) | |
| : this.#currency.exponent | |
| this.#amount = majorToMinor(amount, this.#scale) | |
| } | |
| } | |
| } | |
| /** | |
| * Gets the scale of this Money instance. | |
| */ | |
| get scale(): number { | |
| return this.#scale | |
| } | |
| /** | |
| * Gets the amount in the smallest currency unit as BigInt. | |
| */ | |
| get amount(): bigint { | |
| return this.#amount | |
| } | |
| /** | |
| * Returns the internal amount (#amount) adjusted (scaled) to the currency's exponent. | |
| * If the instance's scale is higher than the currency's exponent (scaling down), | |
| * the amount will be rounded using "round half up". If scaling up, it's padded with zeros. | |
| * @returns The amount as a BigInt, scaled to the currency's exponent. | |
| */ | |
| get scaledAmount(): bigint { | |
| const currentExponent = BigInt(this.#scale) | |
| const currencyExponent = BigInt(this.#currency.exponent) | |
| if (currentExponent === currencyExponent) { | |
| return this.#amount | |
| } | |
| const exponentDifference = currentExponent - currencyExponent | |
| if (exponentDifference > 0n) { | |
| // Current amount has more decimal places (higher precision) than currency's exponent. | |
| // Need to scale down by dividing. Apply "round half up". | |
| const divisor = BigInt(10) ** exponentDifference | |
| const roundingThreshold = divisor / 2n // For "round half up" | |
| // Add half of the divisor before performing integer division. | |
| return (this.#amount + roundingThreshold) / divisor | |
| } else { | |
| // Current amount has fewer decimal places (lower precision) than currency's exponent. | |
| // Need to scale up by multiplying (padding with zeros). | |
| const factor = BigInt(10) ** -exponentDifference // -exponentDifference will be positive | |
| return this.#amount * factor | |
| } | |
| } | |
| /** | |
| * Gets the currency of this Money instance. | |
| */ | |
| get currency(): Currency { | |
| return this.#currency | |
| } | |
| /** | |
| * Creates a Money instance from a BigInt minor unit amount and an explicit scale. | |
| * Useful for constructing results of arithmetic operations. | |
| * @param amount The amount in minor units as BigInt. | |
| * @param currencyCode The currency code. | |
| * @param scale The explicit scale (number of decimal places) for this amount. | |
| * @returns A new Money instance. | |
| */ | |
| static fromMinorAndScale(amount: bigint, currencyCode: string, scale: number): Money { | |
| // Pass true for isMinorUnit and the explicit scale | |
| return new Money(String(amount), currencyCode, true, scale) | |
| } | |
| /** | |
| * Normalizes two Money instances to their highest common scale for arithmetic operations. | |
| * Throws an error if currencies differ. | |
| * @param moneyA The first Money instance. | |
| * @param moneyB The second Money instance. | |
| * @returns An object containing the normalized BigInt amounts and the common scale. | |
| */ | |
| private static normalizeAmounts( | |
| moneyA: Money, | |
| moneyB: Money | |
| ): { amountA: bigint; amountB: bigint; commonScale: number } { | |
| if (moneyA.currency.code !== moneyB.currency.code) { | |
| throw new Error( | |
| `Cannot perform operations on Money instances with different currencies (${moneyA.currency.code} and ${moneyB.currency.code}).` | |
| ) | |
| } | |
| const commonScale = Math.max(moneyA.scale, moneyB.scale) | |
| // Calculate scaling factors | |
| const scaleFactorA = BigInt(10) ** BigInt(commonScale - moneyA.scale) | |
| const scaleFactorB = BigInt(10) ** BigInt(commonScale - moneyB.scale) | |
| // Apply scaling | |
| const amountA = moneyA.amount * scaleFactorA | |
| const amountB = moneyB.amount * scaleFactorB | |
| return { amountA, amountB, commonScale } | |
| } | |
| // Helper to convert non-Money operand to Money instance for internal use | |
| private toMoneyOperand(operand: number | string): Money { | |
| // When converting a number/string operand, use the current instance's currency | |
| // The constructor will derive the correct scale for this temporary Money instance. | |
| return new Money(operand, this.#currency.code) | |
| } | |
| /** | |
| * Private helper to rescale a BigInt amount from one implied scale to another. | |
| * Applies "round half up" if scaling down. | |
| * @param amount The BigInt amount to rescale. | |
| * @param currentImpliedScale The scale this BigInt 'currently' represents. | |
| * @param targetScale The desired scale for the BigInt. | |
| * @returns The rescaled BigInt amount. | |
| */ | |
| private static rescaleBigInt( | |
| amount: bigint, | |
| currentImpliedScale: number, | |
| targetScale: number | |
| ): bigint { | |
| const currentBig = BigInt(currentImpliedScale) | |
| const targetBig = BigInt(targetScale) | |
| if (currentBig === targetBig) { | |
| return amount | |
| } | |
| const exponentDifference = targetBig - currentBig // target - current | |
| if (exponentDifference > 0n) { | |
| // Scaling UP (e.g., from scale 2 to 3 means minor unit becomes smaller) | |
| // Amount needs to be multiplied. | |
| const factor = BigInt(10) ** exponentDifference | |
| return amount * factor | |
| } else { | |
| // Scaling DOWN (e.g., from scale 3 to 2 means minor unit becomes larger) | |
| // Amount needs to be divided, with rounding. | |
| const divisor = BigInt(10) ** -exponentDifference // This is positive | |
| const roundingThreshold = divisor / 2n | |
| // Apply "round half up" | |
| return (amount + roundingThreshold) / divisor | |
| } | |
| } | |
| /** | |
| * Adds another Money instance or a number to this Money instance. | |
| * Ensures scale is taken into consideration for precision. | |
| * @param addend The amount to add. | |
| * @returns A new Money instance with the result. | |
| */ | |
| add(addend: Money | number | string): Money { | |
| const otherMoney = addend instanceof Money ? addend : this.toMoneyOperand(addend) | |
| const { amountA, amountB, commonScale } = Money.normalizeAmounts(this, otherMoney) | |
| const resultAmount = amountA + amountB | |
| return Money.fromMinorAndScale(resultAmount, this.#currency.code, commonScale) | |
| } | |
| /** | |
| * Subtracts another Money instance or a number from this Money instance. | |
| * Ensures scale is taken into consideration for precision. | |
| * @param subtrahend The amount to subtract. | |
| * @returns A new Money instance with the result. | |
| */ | |
| subtract(subtrahend: Money | number | string): Money { | |
| const otherMoney = subtrahend instanceof Money ? subtrahend : this.toMoneyOperand(subtrahend) | |
| const { amountA, amountB, commonScale } = Money.normalizeAmounts(this, otherMoney) | |
| const resultAmount = amountA - amountB | |
| return Money.fromMinorAndScale(resultAmount, this.#currency.code, commonScale) | |
| } | |
| /** | |
| * Multiplies this Money instance by a number. | |
| * The resulting Money instance's scale can be optionally defined by `targetScale`. | |
| * If `targetScale` is not provided, the scale will be the sum of this Money's scale | |
| * and the multiplier's decimal places. | |
| * @param multiplier The number to multiply by. | |
| * @param targetScale Optional: The desired scale of the resulting Money instance. | |
| * @returns A new Money instance with the result. | |
| */ | |
| multiply(multiplier: number | string, targetScale: number | undefined = undefined): Money { | |
| const multiplierStr = String(multiplier) | |
| const [, decimalPart] = multiplierStr.split('.') | |
| const multiplierDecimalPlaces = decimalPart ? decimalPart.length : 0 // Scale of the multiplier number | |
| const multiplierBigInt = majorToMinor(multiplierStr, multiplierDecimalPlaces) // Convert multiplier to a BigInt based on its own precision | |
| // The result of `this.#amount * multiplierBigInt` has an implied scale | |
| // that is the sum of `this.#scale` and `multiplierDecimalPlaces`. | |
| const intermediateProduct = this.#amount * multiplierBigInt | |
| const impliedResultScale = this.#scale + multiplierDecimalPlaces | |
| let finalAmount: bigint | |
| let finalScale: number | |
| if (targetScale !== undefined) { | |
| // If a targetScale is provided, rescale the intermediate product to that target. | |
| finalScale = targetScale | |
| finalAmount = Money.rescaleBigInt(intermediateProduct, impliedResultScale, finalScale) | |
| } else { | |
| // Otherwise, keep the full implied precision. | |
| finalScale = impliedResultScale | |
| finalAmount = intermediateProduct | |
| } | |
| return Money.fromMinorAndScale(finalAmount, this.#currency.code, finalScale) | |
| } | |
| // Inside Money class | |
| /** | |
| * Divides this Money instance by a number. | |
| * The resulting Money instance's scale can be optionally defined by `targetScale`. | |
| * If `targetScale` is not provided, a fixed amount of extra precision is added to reduce truncation. | |
| * @param divisor The number to divide by. Must not be zero. | |
| * @param targetScale Optional: The desired scale of the resulting Money instance. | |
| * @returns A new Money instance with the result. | |
| * @throws Error if divisor is zero. | |
| */ | |
| divide(divisor: number | string, targetScale: number | undefined = undefined): Money { | |
| const divisorMoney = new Money(divisor, this.#currency.code) // Convert divisor to a Money instance to properly get its scale | |
| const divisorAmount = divisorMoney.amount | |
| const divisorScale = divisorMoney.scale | |
| if (divisorAmount === 0n) { | |
| throw new Error('Division by zero.') | |
| } | |
| // Determine a high-precision scale for the preliminary division result, | |
| // before applying any explicit targetScale. | |
| const DIVISION_PRECISION_BUFFER = 6 | |
| const unconstrainedResultScale = | |
| Math.max(this.#scale, this.#currency.exponent) + DIVISION_PRECISION_BUFFER | |
| // Perform the division at high precision. | |
| const numerator = this.#amount * BigInt(10) ** BigInt(divisorScale + unconstrainedResultScale) | |
| const denominator = divisorAmount * BigInt(10) ** BigInt(this.#scale) | |
| const preliminaryResultAmount = numerator / denominator | |
| let finalAmount: bigint | |
| let finalScale: number | |
| if (targetScale !== undefined) { | |
| // If a targetScale is provided, rescale the preliminary result to that target. | |
| finalScale = targetScale | |
| finalAmount = Money.rescaleBigInt( | |
| preliminaryResultAmount, | |
| unconstrainedResultScale, | |
| finalScale | |
| ) | |
| } else { | |
| // Otherwise, keep the high unconstrained precision. | |
| finalScale = unconstrainedResultScale | |
| finalAmount = preliminaryResultAmount | |
| } | |
| return Money.fromMinorAndScale(finalAmount, this.#currency.code, finalScale) | |
| } | |
| /** | |
| * Allocates the money into multiple shares based on ratios. | |
| * Returns shares at the instance's current scale by default, or at a specified `returnScale`. | |
| * @param ratios An array of numbers representing the ratios for allocation. | |
| * @param returnScale Optional: The desired scale for the returned Money instances. Defaults to the instance's current scale. | |
| * @returns An array of new Money instances representing the allocated shares. | |
| */ | |
| allocate(ratios: number[], returnScale: number | undefined = undefined): Money[] { | |
| if (ratios.length === 0) { | |
| return [] | |
| } | |
| const totalRatio = ratios.reduce((sum, ratio) => sum + ratio, 0) | |
| if (totalRatio <= 0) { | |
| throw new Error('Total ratio must be positive for allocation.') | |
| } | |
| let allocatedAmounts: bigint[] = [] | |
| let totalAllocated = 0n | |
| const RATIO_SCALING_FACTOR = BigInt(10) ** BigInt(6) | |
| const totalRatioScaled = BigInt(Math.round(totalRatio * Number(RATIO_SCALING_FACTOR))) | |
| for (const loopRatio of ratios) { | |
| const ratioScaled = BigInt(Math.round(loopRatio * Number(RATIO_SCALING_FACTOR))) | |
| // Shares are calculated at the current instance's scale (this.#scale). | |
| const share = (this.#amount * ratioScaled) / totalRatioScaled | |
| allocatedAmounts.push(share) | |
| totalAllocated += share | |
| } | |
| // Handle potential remainder. | |
| const remainder = this.#amount - totalAllocated | |
| for (let i = 0; i < remainder; i++) { | |
| if (i < allocatedAmounts.length) { | |
| allocatedAmounts[i]++ | |
| } else { | |
| console.warn( | |
| 'Allocation remainder exceeds number of shares. This indicates an issue with remainder calculation or share count.' | |
| ) | |
| } | |
| } | |
| // Determine the final scale for the returned Money instances. | |
| const finalReturnScale = returnScale !== undefined ? returnScale : this.#scale | |
| // Return new Money instances, rescaling each allocated amount to the `finalReturnScale`. | |
| return allocatedAmounts.map((amount) => | |
| Money.fromMinorAndScale( | |
| Money.rescaleBigInt(amount, this.#scale, finalReturnScale), // Rescale amount from original scale to finalReturnScale | |
| this.#currency.code, | |
| finalReturnScale | |
| ) | |
| ) | |
| } | |
| /** | |
| * Distributes the money equally into a specified number of shares. | |
| * Returns shares at the currency's natural exponent scale. | |
| * @param numberOfShares The number of equal shares to distribute into. | |
| * @returns An array of new Money instances representing the equal shares. | |
| * @throws Error if numberOfShares is less than or equal to zero. | |
| */ | |
| distributeEqually(numberOfShares: number): Money[] { | |
| if (numberOfShares <= 0) { | |
| throw new Error('Number of shares must be positive for equal distribution.') | |
| } | |
| const ratios = Array(numberOfShares).fill(1) | |
| // Call allocate, and explicitly ask for the shares to be at the currency's natural exponent. | |
| return this.allocate(ratios, this.#currency.exponent) | |
| } | |
| /** | |
| * Formats the money amount using Intl.NumberFormat. | |
| * @param locale The locale string (default: 'en-ZM'). | |
| * @param options Optional Intl.NumberFormatOptions. | |
| * @returns The formatted string. | |
| */ | |
| format(locale: string = 'en-ZM', options: Intl.NumberFormatOptions = {}): string { | |
| const formatter = getFormatter(this.#currency, locale, options) | |
| // Convert the BigInt minor unit amount to a major unit number for formatting. | |
| // This might involve precision loss for display, which is acceptable for formatting. | |
| return formatter.format(Number(this.toDecimal())) | |
| } | |
| /** | |
| * Returns the amount in major units as a number. | |
| * Note: This conversion can lead to floating-point inaccuracies for display purposes. | |
| * Use `format` or `toDecimal` for precise string representation. | |
| * @returns The amount in major units as a number. | |
| */ | |
| toNumber(): number { | |
| return Number(this.toDecimal()) // Safer to convert `toDecimal()` string to number | |
| } | |
| /** | |
| * Returns the amount in major units as a string. | |
| * This avoids potential floating-point issues of `toNumber()` and respects the instance's scale. | |
| * @returns The amount in major units as a string. | |
| */ | |
| toDecimal(): string { | |
| const divisor = BigInt(10) ** BigInt(this.#scale) // Use this.#scale here | |
| const majorPart = this.#amount / divisor | |
| let minorPart = this.#amount % divisor | |
| // Handle negative numbers for the minor part. If total amount is negative, | |
| // the remainder might also be negative, but we want the absolute minor part for display. | |
| if (minorPart < 0n) { | |
| minorPart = -minorPart | |
| } | |
| // Pad the minor part with leading zeros to match the current instance's scale | |
| let minorString = minorPart.toString().padStart(this.#scale, '0') | |
| // If the minor part is '0', padStart will return '0' for exponent 0, or '00' for exponent 2 etc. | |
| // But we might have a minor part that is, e.g., '5' for scale 2, should be '50'. | |
| // `padStart(this.#scale, '0')` should work correctly for this. | |
| // Combine integer and decimal parts | |
| let resultString = `${majorPart}.${minorString}` | |
| // If the amount is negative and the major part is 0 (e.g., -0.5), ensure the negative sign is there. | |
| if (this.#amount < 0n && majorPart === 0n && minorPart !== 0n) { | |
| resultString = `-${resultString.replace('-', '')}` | |
| } | |
| // If scale is 0, no decimal point is needed | |
| if (this.#scale === 0) { | |
| return this.#amount.toString() | |
| } | |
| return resultString | |
| } | |
| // Helper to safely convert 'other' operand to a Money instance for comparison | |
| private getOtherMoneyForComparison(other: Money | number | string): Money { | |
| if (other instanceof Money) { | |
| if (other.currency.code !== this.#currency.code) { | |
| throw new Error( | |
| `Cannot compare Money instances with different currencies (${this.#currency.code} and ${other.currency.code}).` | |
| ) | |
| } | |
| return other | |
| } else { | |
| // Create a temporary Money instance for the number/string operand. | |
| // The constructor will derive its scale correctly. | |
| return new Money(other, this.#currency.code) | |
| } | |
| } | |
| /** | |
| * Helper to safely convert 'other' operand to a Money instance for operations | |
| * where currency check is explicitly ignored (e.g., haveSameAmount). | |
| * It creates the Money instance using the current instance's currency code. | |
| * @param other The other operand (Money, number, or string). | |
| * @returns A Money instance representing the other operand. | |
| */ | |
| private getOtherMoneyNoCurrencyCheck(other: Money | number | string): Money { | |
| if (other instanceof Money) { | |
| return other // Already a Money instance | |
| } else { | |
| // For number/string, create a Money instance using this instance's currency. | |
| // The constructor will derive the appropriate scale for the new Money instance. | |
| return new Money(other, this.#currency.code) | |
| } | |
| } | |
| /** | |
| * Checks if this Money instance is less than another. | |
| * Compares values after normalizing to a common scale. | |
| * @param other The other Money instance or number to compare with. | |
| * @returns True if this instance is less than the other. | |
| */ | |
| lessThan(other: Money | number | string): boolean { | |
| const otherMoney = this.getOtherMoneyForComparison(other) | |
| const { amountA, amountB } = Money.normalizeAmounts(this, otherMoney) | |
| return amountA < amountB | |
| } | |
| /** | |
| * Checks if this Money instance is less than or equal to another. | |
| * Compares values after normalizing to a common scale. | |
| * @param other The other Money instance or number to compare with. | |
| * @returns True if this instance is less than or equal to the other. | |
| */ | |
| lessThanOrEqual(other: Money | number | string): boolean { | |
| const otherMoney = this.getOtherMoneyForComparison(other) | |
| const { amountA, amountB } = Money.normalizeAmounts(this, otherMoney) | |
| return amountA <= amountB | |
| } | |
| /** | |
| * Checks if this Money instance is equal to another. | |
| * Currency must also be the same, and values are compared after normalizing to a common scale. | |
| * @param other The other Money instance or number to compare with. | |
| * @returns True if this instance is equal to the other. | |
| */ | |
| equal(other: Money | number | string): boolean { | |
| if (other instanceof Money) { | |
| if (other.currency.code !== this.#currency.code) { | |
| // If the other operand is a Money instance and currencies differ, they can't be equal. | |
| return false | |
| } | |
| } | |
| const otherMoney = this.getOtherMoneyForComparison(other) // Will throw if currency differs for non-Money | |
| const { amountA, amountB } = Money.normalizeAmounts(this, otherMoney) | |
| return amountA === amountB | |
| } | |
| /** | |
| * Checks if this Money instance is greater than another. | |
| * Compares values after normalizing to a common scale. | |
| * @param other The other Money instance or number to compare with. | |
| * @returns True if this instance is greater than the other. | |
| */ | |
| greaterThan(other: Money | number | string): boolean { | |
| const otherMoney = this.getOtherMoneyForComparison(other) | |
| const { amountA, amountB } = Money.normalizeAmounts(this, otherMoney) | |
| return amountA > amountB | |
| } | |
| /** | |
| * Checks if this Money instance is greater than or equal to another. | |
| * Compares values after normalizing to a common scale. | |
| * @param other The other Money instance or number to compare with. | |
| * @returns True if this instance is greater than or equal to the other. | |
| */ | |
| greaterThanOrEqual(other: Money | number | string): boolean { | |
| const otherMoney = this.getOtherMoneyForComparison(other) | |
| const { amountA, amountB } = Money.normalizeAmounts(this, otherMoney) | |
| return amountA >= amountB | |
| } | |
| /** | |
| * Checks if the amount is negative. | |
| * @returns True if the amount is less than zero. | |
| */ | |
| isNegative(): boolean { | |
| return this.#amount < 0n | |
| } | |
| /** | |
| * Checks if the amount is zero. | |
| * @returns True if the amount is equal to zero. | |
| */ | |
| isZero(): boolean { | |
| return this.#amount === 0n | |
| } | |
| /** | |
| * Checks if the amount is positive. | |
| * @returns True if the amount is greater than zero. | |
| */ | |
| isPositive(): boolean { | |
| return this.#amount > 0n | |
| } | |
| /** | |
| * Returns the absolute value of the money amount. | |
| * @returns A new Money instance with the absolute value, maintaining the original scale. | |
| */ | |
| absolute(): Money { | |
| const absAmount = this.#amount < 0n ? -this.#amount : this.#amount | |
| return Money.fromMinorAndScale(absAmount, this.#currency.code, this.#scale) | |
| } | |
| /** | |
| * Finds the minimum among this Money instance and others. | |
| * All instances must have the same currency. Compares values after normalizing to a common scale. | |
| * @param others Other Money instances or numbers to compare. | |
| * @returns The minimum Money instance (the actual object that was found to be the minimum). | |
| * @throws Error if currencies differ. | |
| */ | |
| minimum(...others: Array<Money | number | string>): Money { | |
| let minMoney = Money.fromMinorAndScale(this.#amount, this.#currency.code, this.#scale) // Start with this instance as the current minimum candidate | |
| for (const other of others) { | |
| const otherMoney = this.getOtherMoneyForComparison(other) // Safely get other as Money instance | |
| // Normalize current minimum candidate and the other Money instance for comparison | |
| const { amountA, amountB } = Money.normalizeAmounts(minMoney, otherMoney) | |
| if (amountB < amountA) { | |
| minMoney = otherMoney // If other is smaller, it becomes the new minimum candidate | |
| } | |
| } | |
| // Return the Money instance that was determined to be the minimum. | |
| // It retains its original scale. | |
| return minMoney | |
| } | |
| /** | |
| * Finds the maximum among this Money instance and others. | |
| * All instances must have the same currency. Compares values after normalizing to a common scale. | |
| * @param others Other Money instances or numbers to compare. | |
| * @returns The maximum Money instance (the actual object that was found to be the maximum). | |
| * @throws Error if currencies differ. | |
| */ | |
| maximum(...others: Array<Money | number | string>): Money { | |
| let maxMoney = Money.fromMinorAndScale(this.#amount, this.#currency.code, this.#scale) // Start with this instance as the current maximum candidate | |
| for (const other of others) { | |
| const otherMoney = this.getOtherMoneyForComparison(other) // Safely get other as Money instance | |
| // Normalize current maximum candidate and the other Money instance for comparison | |
| const { amountA, amountB } = Money.normalizeAmounts(maxMoney, otherMoney) | |
| if (amountB > amountA) { | |
| maxMoney = otherMoney // If other is larger, it becomes the new maximum candidate | |
| } | |
| } | |
| // Return the Money instance that was determined to be the maximum. | |
| // It retains its original scale. | |
| return maxMoney | |
| } | |
| /** | |
| * Checks if this Money instance and others have the same numerical amount. | |
| * Currency is ignored for this check. Values are compared after normalizing to a common scale. | |
| * @param others Other Money instances or numbers to compare. | |
| * @returns True if all instances have the same numerical amount. | |
| */ | |
| haveSameAmount(...others: Array<Money | number | string>): boolean { | |
| // Convert this instance's amount to BigInt at its current scale | |
| const thisAmount = this.#amount | |
| const thisScale = this.#scale | |
| for (const other of others) { | |
| const otherMoney = this.getOtherMoneyNoCurrencyCheck(other) // Get Money instance, no currency check | |
| // Normalize amounts to the highest common scale for comparison | |
| // The normalizeAmounts function expects both operands to be Money instances | |
| // and internally handles the scaling of their BigInt amounts. | |
| // Although normalizeAmounts has a currency check, we are passing in Money | |
| // instances that either have the same currency (if from `this`), or | |
| // a temporary Money instance created with `this.#currency.code`. | |
| // So the check passes for a *technical* match of currency code in the Money object, | |
| // allowing the value comparison to proceed regardless of actual real-world currency. | |
| const { amountA, amountB } = Money.normalizeAmounts( | |
| Money.fromMinorAndScale(thisAmount, this.#currency.code, thisScale), // Create temp Money from `this` | |
| otherMoney | |
| ) | |
| if (amountA !== amountB) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| /** | |
| * Checks if this Money instance and others have the same currency. | |
| * @param others Other Money instances to compare. | |
| * @returns True if all instances have the same currency code. | |
| */ | |
| haveSameCurrency(...others: Array<Money>): boolean { | |
| for (const other of others) { | |
| if (this.#currency.code !== other.currency.code) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| /** | |
| * Checks if the amount has subunits (i.e., a non-zero minor unit component). | |
| * @returns True if the amount has a non-zero remainder when divided by 10^exponent. | |
| */ | |
| hasSubUnits(): boolean { | |
| const divisor = BigInt(10) ** BigInt(this.#currency.exponent) | |
| return this.#amount % divisor !== 0n | |
| } | |
| /** | |
| * Converts this Money instance to a different currency, maintaining precision and applying rounding. | |
| * | |
| * @param targetCurrencyCode The currency code to convert to. | |
| * @param conversionRate The rate to multiply by (e.g., 1 USD = 0.85 EUR, rate is 0.85 when converting USD to EUR). | |
| * @returns A new Money instance in the target currency. | |
| * @throws Error if the target currency is not supported or conversion rate is invalid. | |
| */ | |
| convert(targetCurrencyCode: string, conversionRate: number | string): Money { | |
| const targetCurrency = currencies[targetCurrencyCode] | |
| if (!targetCurrency) { | |
| throw new Error(`Unsupported target currency code for conversion: ${targetCurrencyCode}`) | |
| } | |
| const rateNum = Number(conversionRate) | |
| if (Number.isNaN(rateNum) || rateNum <= 0) { | |
| throw new Error('Invalid conversion rate: Must be a positive number.') | |
| } | |
| if (this.#currency.code === targetCurrency.code) { | |
| // If converting to the same currency, create a new instance with the target currency's exponent | |
| // to normalize its scale, but maintain the original value. | |
| // This ensures the resulting Money object reflects the target currency's official precision. | |
| // We explicitly convert to a BigInt at the target currency's exponent. | |
| const convertedAmountAtTargetExponent = majorToMinor( | |
| this.toDecimal(), | |
| targetCurrency.exponent | |
| ) | |
| return Money.fromMinorAndScale( | |
| convertedAmountAtTargetExponent, | |
| targetCurrency.code, | |
| targetCurrency.exponent | |
| ) | |
| } | |
| // Determine the scale of the conversion rate itself. | |
| const conversionRateStr = String(conversionRate) | |
| const [, rateDecimalPart] = conversionRateStr.split('.') | |
| const rateDecimalPlaces = rateDecimalPart ? rateDecimalPart.length : 0 | |
| // Convert the conversion rate to a BigInt based on its decimal places. | |
| const rateBigInt = BigInt( | |
| `${rateDecimalPart ? conversionRateStr.replace('.', '') : conversionRateStr}` | |
| ) | |
| // Multiply the current Money's internal amount by the rate BigInt. | |
| // The current amount (#amount) is at `this.#scale` precision. | |
| // The `rateBigInt` implicitly represents `rateNum * (10 ** rateDecimalPlaces)`. | |
| // So, `this.#amount * rateBigInt` will be scaled by `this.#scale + rateDecimalPlaces`. | |
| const intermediateProduct = this.#amount * rateBigInt | |
| const intermediateProductScale = this.#scale + rateDecimalPlaces | |
| // Calculate the required scaling factor to bring the `intermediateProduct` | |
| // from its `intermediateProductScale` down to the `targetCurrency.exponent`. | |
| const scaleDifference = intermediateProductScale - targetCurrency.exponent | |
| let finalConvertedAmount: bigint | |
| if (scaleDifference > 0) { | |
| // We need to scale down (divide) the `intermediateProduct`. | |
| // Apply "round half up" before division to truncate excess precision. | |
| const powerOfTenToDivideBy = BigInt(10) ** BigInt(scaleDifference) | |
| const roundingThreshold = powerOfTenToDivideBy / 2n // For "round half up" | |
| finalConvertedAmount = (intermediateProduct + roundingThreshold) / powerOfTenToDivideBy | |
| } else if (scaleDifference < 0) { | |
| // We need to scale up (multiply) the `intermediateProduct`. | |
| const powerOfTenToMultiplyBy = BigInt(10) ** BigInt(Math.abs(scaleDifference)) | |
| finalConvertedAmount = intermediateProduct * powerOfTenToMultiplyBy | |
| } else { | |
| // No scaling difference, use the product as is. | |
| finalConvertedAmount = intermediateProduct | |
| } | |
| // Create a new Money instance using the target currency and its natural exponent as the scale. | |
| // The finalConvertedAmount is already scaled to the target currency's exponent. | |
| return Money.fromMinorAndScale( | |
| finalConvertedAmount, | |
| targetCurrency.code, | |
| targetCurrency.exponent | |
| ) | |
| } | |
| // Note: Methods like normalizeScale, transformScale, trimScale from Dinero.js | |
| // relate to managing the internal scale of the Dinero object. | |
| // With our BigInt approach representing a fixed minor unit based on currency exponent, | |
| // these concepts might map differently or be less necessary. | |
| // The `toDecimal()` and `toNumber()` methods handle conversion to major units for display. | |
| // If scale manipulation on the internal BigInt is needed, it would involve | |
| // multiplying or dividing the BigInt by powers of 10 and potentially updating the currency exponent, | |
| // which changes the meaning of the BigInt amount. | |
| // Implementing transformScale as an example: | |
| /** | |
| * Transforms the internal scale of the money amount. | |
| * This changes the unit that the internal BigInt represents. Use with caution. | |
| * When scaling down (newExponent < currentExponent), "round half up" is applied. | |
| * @param newExponent The new currency exponent (scale) to transform to. | |
| * @returns A new Money instance with the transformed scale. | |
| * @throws Error if newExponent is negative. | |
| */ | |
| transformScale(newExponent: number): Money { | |
| if (newExponent < 0) { | |
| throw new Error('New exponent cannot be negative.') | |
| } | |
| const currentExponent = BigInt(this.#scale) | |
| const targetExponent = BigInt(newExponent) | |
| if (targetExponent === currentExponent) { | |
| // No change in scale, return a clone for immutability, preserving the current scale. | |
| return Money.fromMinorAndScale(this.#amount, this.#currency.code, this.#scale) | |
| } | |
| const exponentDifference = targetExponent - currentExponent | |
| let transformedAmount: bigint | |
| if (exponentDifference > 0n) { | |
| // Scaling up the exponent means the minor unit becomes smaller. | |
| // The BigInt amount needs to be multiplied to represent the same value in smaller units. | |
| // E.g., 1.23 (scale 2, amount 123n) to scale 3 (1.230): amount 123n * 10^1 = 1230n | |
| const factor = BigInt(10) ** exponentDifference | |
| transformedAmount = this.#amount * factor | |
| } else { | |
| // Scaling down the exponent means the minor unit becomes larger. | |
| // The BigInt amount needs to be divided to represent the same value in larger units. | |
| // E.g., 1.234 (scale 3, amount 1234n) to scale 2 (1.23): amount 1234n / 10^1 | |
| // We need to apply "round half up" here. | |
| const divisor = BigInt(10) ** -exponentDifference // -exponentDifference will be positive | |
| // Apply "round half up": add half of the divisor before integer division. | |
| const roundingThreshold = divisor / 2n | |
| transformedAmount = (this.#amount + roundingThreshold) / divisor | |
| } | |
| // Create a new Money instance with the transformed amount and the new currency representation. | |
| // The amount is already in the units of the new exponent, and the new instance's scale | |
| // should reflect this new exponent. | |
| return Money.fromMinorAndScale(transformedAmount, this.#currency.code, newExponent) | |
| } | |
| // JSON serialization | |
| toJSON(): { amount: string; currency: Currency; scale: number } { | |
| return { | |
| amount: this.#amount.toString(), // Store BigInt as string | |
| currency: this.#currency, | |
| scale: this.#scale, // Store the instance's scale for faithful deserialization | |
| } | |
| } | |
| static fromJSON(json: { amount: string; currency: Currency; scale: number }): Money { | |
| // Use fromMinorAndScale to reconstruct with the correct BigInt amount and explicit scale | |
| return Money.fromMinorAndScale(BigInt(json.amount), json.currency.code, json.scale) | |
| } | |
| // Example of a static helper to create Money from major unit number | |
| static fromMajor( | |
| amount: number | string, | |
| currencyCode: string = 'ZMW', | |
| scale: number | undefined = undefined | |
| ): Money { | |
| return new Money(amount, currencyCode, false, scale) | |
| } | |
| // Example of a static helper to create Money from minor unit BigInt | |
| static fromMinor( | |
| amount: bigint, | |
| currencyCode: string = 'ZMW', | |
| scale: number | undefined = undefined | |
| ): Money { | |
| return new Money(String(amount), currencyCode, true, scale) | |
| } | |
| // --- Formula Evaluation --- | |
| // Adapting the evaluateFormula from the original file. | |
| // This part still uses `mathjs` and might require careful handling if full BigInt precision | |
| // is needed within the formula evaluation itself. `mathjs` primarily works with numbers or its own BigNumber type. | |
| // For this implementation, I'll keep the structure but note that mathjs might lose precision | |
| // if the intermediate results exceed standard number limits or require decimal precision that mathjs handles with floats. | |
| // A truly BigInt-based formula evaluation would require a different math expression parser. | |
| /** | |
| * Evaluates a mathematical formula with given variables. | |
| * Note: Uses `mathjs` which may not provide full BigInt precision for complex expressions. | |
| * Intermediate calculations in mathjs are typically done with numbers or its own BigNumber type. | |
| * The final result is converted back to Money. | |
| * @param formula The mathematical formula string (e.g., "a + b * c"). | |
| * @param variables An object mapping variable names to their numeric values. | |
| * @param currencyCode The currency code for the result (default: 'ZMW'). | |
| * @param debug Optional debug flag. | |
| * @returns A new Money instance representing the result of the formula. | |
| * @throws Error if formula evaluation fails. | |
| */ | |
| // static evaluateFormula( | |
| // formula: string, | |
| // variables: { [key: string]: number }, | |
| // currencyCode: string = 'ZMW', | |
| // debug: boolean = false // Retain debug flag from original | |
| // ): Money { | |
| // if (!formula) { | |
| // return new Money(0, currencyCode) | |
| // } | |
| // try { | |
| // // Use mathjs to evaluate the formula with numeric variables | |
| // const result = evaluate(formula, variables) | |
| // if (debug) { | |
| // // Placeholder for logger if needed, adapting from original | |
| // // console.debug('Money.evaluateFormula amount:', result); | |
| // } | |
| // // Convert the numeric result from mathjs back to a Money instance | |
| // // This conversion from number to minor unit BigInt is where precision | |
| // // is enforced according to the currency exponent. | |
| // // Note: If `result` is a complex number or other non-numeric type from mathjs, | |
| // // this conversion might fail or need additional handling. | |
| // if (typeof result !== 'number') { | |
| // // Handle cases where mathjs returns non-numeric results (e.g., complex numbers, matrices) | |
| // // For a money library, non-numeric results are typically invalid. | |
| // throw new Error(`Formula evaluation returned a non-numeric result: ${typeof result}`) | |
| // } | |
| // return new Money(result, currencyCode) | |
| // } catch (error) { | |
| // // Placeholder for logger if needed | |
| // // console.error('Money.evaluateFormula error:', error); | |
| // // Return zero money on error, similar to the original behavior | |
| // return new Money(0, currencyCode) | |
| // } | |
| // } | |
| // Note: Methods like `toSnapshot` and `fromSnapshot` from Dinero.js | |
| // map to the internal state representation. With our BigInt approach, | |
| // `toJSON` and `fromJSON` serve a similar purpose for serialization. | |
| // Note: Methods like `toUnits` from Dinero.js break down the amount into major and minor units. | |
| // This can be implemented by converting the BigInt amount. | |
| /** | |
| * Returns the amount broken down into major and minor units based on the instance's scale. | |
| * For example, if amount is 1.23456 (with #amount = 123456n and #scale = 5), returns { major: 1n, minor: 23456n }. | |
| * The major part carries the sign, while the minor part represents the absolute value of the fractional part. | |
| * @returns An object containing major and minor unit amounts as BigInt. | |
| */ | |
| toUnits(): { major: bigint; minor: bigint } { | |
| const divisor = BigInt(10) ** BigInt(this.#scale) // Use this.#scale for breakdown | |
| const majorPart = this.#amount / divisor | |
| let minorPart = this.#amount % divisor | |
| // If the total amount is negative, the remainder (minorPart) might be negative. | |
| // We want the minorPart to represent the absolute value of the fractional part. | |
| if (minorPart < 0n) { | |
| minorPart = -minorPart | |
| } | |
| return { major: majorPart, minor: minorPart } | |
| } | |
| // Note: `addPercentage` and `subtractPercentage` from the original could be implemented | |
| // by calculating the percentage amount using `multiply` and then adding/subtracting. | |
| /** | |
| * Adds a percentage to the money amount. | |
| * @param percentage The percentage to add (e.g., 10 for 10%). Can be decimal. | |
| * @returns A new Money instance with the percentage added. | |
| */ | |
| addPercentage(percentage: number | string): Money { | |
| // Calculate the percentage amount | |
| // Treat percentage as a multiplier: amount * (percentage / 100) | |
| const percentageAmount = this.multiply(Number(percentage) / 100) | |
| // Add the calculated percentage amount to the original amount | |
| return this.add(percentageAmount) | |
| } | |
| /** | |
| * Subtracts a percentage from the money amount. | |
| * @param percentage The percentage to subtract (e.g., 10 for 10%). Can be decimal. | |
| * @returns A new Money instance with the percentage subtracted. | |
| */ | |
| subtractPercentage(percentage: number | string): Money { | |
| // Calculate the percentage amount | |
| const percentageAmount = this.multiply(Number(percentage) / 100) | |
| // Subtract the calculated percentage amount from the original amount | |
| return this.subtract(percentageAmount) | |
| } | |
| // Re-implement distributeEquallyWithTax and distributeEquallyWithTaxAboveMinimum | |
| // based on the new allocate and arithmetic methods. This involves more complex | |
| // allocation logic including tax and minimums, similar to the original. | |
| /** | |
| * Distributes the money equally with tax consideration. | |
| * Calculates tax based on percentage, adds it to the first share, distributes the rest of the original amount. | |
| * Optionally enforces a minimum amount for the first share. | |
| * @param taxPercentage The percentage to add/subtract (e.g., 10 for 10%). Can be decimal. | |
| * @param numberOfShares The number of shares to distribute into. | |
| * @param isInclusiveTax If true, the tax percentage is already included in the initial amount. Defaults to false (exclusive). | |
| * @param minimum Optional minimum amount (in major units) for the first share. | |
| * @returns An array of new Money instances representing the allocated shares. | |
| * @throws Error if numberOfShares is less than or equal to zero. | |
| */ | |
| distributeEquallyWithTax( | |
| taxPercentage: number | string, | |
| numberOfShares: number, | |
| isInclusiveTax: boolean = false, | |
| minimum: number | string | Money | undefined = undefined | |
| ): Money[] { | |
| if (numberOfShares <= 0) { | |
| throw new Error('Number of shares must be positive for equal distribution.') | |
| } | |
| const taxRate = Number(taxPercentage) / 100 | |
| let taxAmount: Money | |
| let amountToDistributeEqually: Money | |
| if (isInclusiveTax) { | |
| // If tax is inclusive, the tax amount is part of the total. | |
| // We need to back-calculate the tax and the net amount. | |
| // netAmount = total / (1 + taxRate) | |
| // taxAmount = total - netAmount | |
| const onePlusTaxRate = 1 + taxRate | |
| if (onePlusTaxRate === 0) { | |
| throw new Error( | |
| 'Invalid tax rate for inclusive tax calculation (1 + taxRate cannot be zero).' | |
| ) | |
| } | |
| const netAmount = this.divide(onePlusTaxRate, this.#currency.exponent) // Divide by 1 + rate to get net. | |
| taxAmount = this.subtract(netAmount) // Tax is the difference. | |
| amountToDistributeEqually = netAmount | |
| } else { | |
| // If tax is exclusive, the tax amount is calculated on the total. | |
| taxAmount = this.multiply(taxRate, this.#currency.exponent) | |
| amountToDistributeEqually = Money.fromMinorAndScale( | |
| this.#amount, | |
| this.#currency.code, | |
| this.#scale | |
| ) | |
| } | |
| // Distribute the `amountToDistributeEqually` equally among all shares. | |
| const sharesBeforeTaxAllocation = amountToDistributeEqually.distributeEqually(numberOfShares) | |
| // Add the calculated tax amount ONLY to the first share. | |
| const allocatedShares = sharesBeforeTaxAllocation.map((share, index) => | |
| index === 0 ? share.add(taxAmount) : share | |
| ) | |
| // Handle minimum logic (if applicable) | |
| if (minimum !== undefined) { | |
| const minimumMoney = this.getOtherMoneyForComparison(minimum) | |
| // Check if the first calculated share (with tax) is less than the minimum. | |
| if (allocatedShares[0].lessThan(minimumMoney)) { | |
| const enforcedFirstShare = Money.fromMinorAndScale( | |
| majorToMinor(minimumMoney.toDecimal(), this.#scale), | |
| this.#currency.code, | |
| this.#scale | |
| ) | |
| // The remaining amount is the ORIGINAL total amount minus the enforced first share. | |
| // This remains `this.subtract(...)` as the minimum enforcement is against the original total. | |
| const remainingAmount = this.subtract(enforcedFirstShare) | |
| if (numberOfShares > 1) { | |
| const restShares = remainingAmount.distributeEqually(numberOfShares - 1) | |
| return [enforcedFirstShare, ...restShares] | |
| } else { | |
| return [enforcedFirstShare] | |
| } | |
| } | |
| } | |
| return allocatedShares | |
| } | |
| // Inside Money class | |
| /** | |
| * Distributes the money equally with tax consideration, enforcing a minimum on the first share. | |
| * Calculates tax based on percentage, adds it to the first share, distributes the rest of the amount. | |
| * This method always applies the minimum logic. | |
| * @param taxPercentage The percentage to add/subtract (e.g., 10 for 10%). Can be decimal. | |
| * @param numberOfShares The number of shares to distribute into. | |
| * @param isInclusiveTax If true, the tax percentage is already included in the initial amount. Defaults to false (exclusive). | |
| * @param minimum The minimum amount (in major units) for the first share. | |
| * @returns An array of new Money instances representing the allocated shares. | |
| * @throws Error if numberOfShares is less than or equal to zero. | |
| */ | |
| distributeEquallyWithTaxAboveMinimum( | |
| taxPercentage: number | string, | |
| numberOfShares: number, | |
| isInclusiveTax: boolean = false, // NEW PARAMETER | |
| minimum: number | string | Money | |
| ): Money[] { | |
| if (numberOfShares <= 0) { | |
| throw new Error('Number of shares must be positive for equal distribution.') | |
| } | |
| const taxRate = Number(taxPercentage) / 100 | |
| let taxAmount: Money | |
| let amountToDistributeEqually: Money // This will be `this` or `netAmount` | |
| if (isInclusiveTax) { | |
| const onePlusTaxRate = 1 + taxRate | |
| if (onePlusTaxRate === 0) { | |
| throw new Error( | |
| 'Invalid tax rate for inclusive tax calculation (1 + taxRate cannot be zero).' | |
| ) | |
| } | |
| const netAmount = this.divide(onePlusTaxRate, this.#currency.exponent) | |
| taxAmount = this.subtract(netAmount) | |
| amountToDistributeEqually = netAmount | |
| } else { | |
| taxAmount = this.multiply(taxRate, this.#currency.exponent) | |
| amountToDistributeEqually = Money.fromMinorAndScale( | |
| this.#amount, | |
| this.#currency.code, | |
| this.#scale | |
| ) | |
| } | |
| // Distribute the `amountToDistributeEqually` equally among all shares. | |
| const sharesBeforeTaxAllocation = amountToDistributeEqually.distributeEqually(numberOfShares) | |
| // Add the calculated tax amount ONLY to the first share. | |
| const allocatedShares = sharesBeforeTaxAllocation.map((share, index) => | |
| index === 0 ? share.add(taxAmount) : share | |
| ) | |
| // Handle minimum logic (which is always applied in this method) | |
| const minimumMoney = this.getOtherMoneyForComparison(minimum) | |
| if (allocatedShares[0].lessThan(minimumMoney)) { | |
| const enforcedFirstShare = Money.fromMinorAndScale( | |
| majorToMinor(minimumMoney.toDecimal(), this.#scale), | |
| this.#currency.code, | |
| this.#scale | |
| ) | |
| // The remaining amount is the ORIGINAL total amount minus the enforced first share. | |
| const remainingAmount = this.subtract(enforcedFirstShare) | |
| if (numberOfShares > 1) { | |
| const restShares = remainingAmount.distributeEqually(numberOfShares - 1) | |
| return [enforcedFirstShare, ...restShares] | |
| } else { | |
| return [enforcedFirstShare] | |
| } | |
| } | |
| return allocatedShares | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment