feat(core): initial release of financial calculation package with decimal precision
Some checks failed
CI Pipeline (nottags) / security (push) Successful in 17s
CI Pipeline (tags) / security (push) Successful in 17s
CI Pipeline (nottags) / test (push) Failing after 52s
CI Pipeline (tags) / test (push) Failing after 50s
CI Pipeline (tags) / release (push) Has been skipped
CI Pipeline (tags) / metadata (push) Has been skipped

This commit is contained in:
2025-07-29 09:20:06 +00:00
commit d63339cb71
22 changed files with 12531 additions and 0 deletions

8
ts/00_commitinfo_data.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@fin.cx/calculation',
version: '1.0.0',
description: 'A unified, cross-platform financial calculation package with decimal precision for accurate financial computations'
};

View File

@@ -0,0 +1,313 @@
import * as plugins from './calculation.plugins.js';
import { Calculator } from './calculation.classes.calculator.js';
export interface IAmortizationPayment {
period: number;
payment: plugins.Decimal;
principal: plugins.Decimal;
interest: plugins.Decimal;
balance: plugins.Decimal;
cumulativePrincipal: plugins.Decimal;
cumulativeInterest: plugins.Decimal;
}
export interface IAmortizationSchedule {
payments: IAmortizationPayment[];
totalPayment: plugins.Decimal;
totalPrincipal: plugins.Decimal;
totalInterest: plugins.Decimal;
monthlyPayment: plugins.Decimal;
}
export interface ILoanOptions {
principal: plugins.Decimal.Value;
annualRate: plugins.Decimal.Value;
termYears?: plugins.Decimal.Value;
termMonths?: plugins.Decimal.Value;
paymentFrequency?: 'monthly' | 'biweekly' | 'weekly';
extraPayment?: plugins.Decimal.Value;
}
/**
* Amortization calculations class for loan schedules and payment analysis
*/
export class Amortization extends Calculator {
constructor() {
super({ precision: 15 });
}
/**
* Generate a complete amortization schedule
* @param options Loan options
*/
public schedule(options: ILoanOptions): IAmortizationSchedule {
const principal = this.decimal(options.principal);
const annualRate = this.decimal(options.annualRate);
const termMonths = this.calculateTermMonths(options);
const extraPayment = options.extraPayment ? this.decimal(options.extraPayment) : this.decimal(0);
const monthlyRate = this.divide(annualRate, 12);
const monthlyPayment = this.calculatePayment(principal, monthlyRate, termMonths);
const payments: IAmortizationPayment[] = [];
let balance = principal;
let cumulativePrincipal = this.decimal(0);
let cumulativeInterest = this.decimal(0);
for (let period = 1; period <= termMonths.toNumber() && balance.greaterThan(0); period++) {
const interestPayment = this.multiply(balance, monthlyRate);
const scheduledPrincipal = this.subtract(monthlyPayment, interestPayment);
// Apply extra payment to principal
const totalPrincipal = this.add(scheduledPrincipal, extraPayment);
// Ensure we don't pay more than the remaining balance
const actualPrincipal = this.min(totalPrincipal, balance);
const actualPayment = this.add(interestPayment, actualPrincipal);
balance = this.subtract(balance, actualPrincipal);
cumulativePrincipal = this.add(cumulativePrincipal, actualPrincipal);
cumulativeInterest = this.add(cumulativeInterest, interestPayment);
payments.push({
period,
payment: actualPayment,
principal: actualPrincipal,
interest: interestPayment,
balance: balance,
cumulativePrincipal: cumulativePrincipal,
cumulativeInterest: cumulativeInterest
});
// If balance is paid off early, stop
if (balance.isZero()) {
break;
}
}
const totalPayment = payments.reduce((sum, p) => this.add(sum, p.payment), this.decimal(0));
const totalPrincipal = cumulativePrincipal;
const totalInterest = cumulativeInterest;
return {
payments,
totalPayment,
totalPrincipal,
totalInterest,
monthlyPayment
};
}
/**
* Calculate monthly payment for a loan
*/
private calculatePayment(
principal: plugins.Decimal,
monthlyRate: plugins.Decimal,
termMonths: plugins.Decimal
): plugins.Decimal {
if (monthlyRate.isZero()) {
return this.divide(principal, termMonths);
}
const numerator = this.multiply(principal, monthlyRate);
const denominator = this.subtract(
1,
this.power(this.add(1, monthlyRate), this.multiply(-1, termMonths))
);
return this.divide(numerator, denominator);
}
/**
* Calculate total term in months
*/
private calculateTermMonths(options: ILoanOptions): plugins.Decimal {
if (options.termMonths) {
return this.decimal(options.termMonths);
} else if (options.termYears) {
return this.multiply(this.decimal(options.termYears), 12);
} else {
throw new Error('Either termYears or termMonths must be specified');
}
}
/**
* Calculate remaining balance after a specific number of payments
* @param principal Original loan amount
* @param annualRate Annual interest rate
* @param termMonths Total term in months
* @param paymentsMade Number of payments made
*/
public remainingBalance(
principal: plugins.Decimal.Value,
annualRate: plugins.Decimal.Value,
termMonths: plugins.Decimal.Value,
paymentsMade: plugins.Decimal.Value
): plugins.Decimal {
const p = this.decimal(principal);
const r = this.divide(this.decimal(annualRate), 12);
const n = this.decimal(termMonths);
const m = this.decimal(paymentsMade);
if (r.isZero()) {
const paymentAmount = this.divide(p, n);
const paidAmount = this.multiply(paymentAmount, m);
return this.subtract(p, paidAmount);
}
const onePlusR = this.add(1, r);
const factor1 = this.power(onePlusR, n);
const factor2 = this.power(onePlusR, m);
const numerator = this.multiply(p, this.subtract(factor1, factor2));
const denominator = this.subtract(factor1, 1);
return this.divide(numerator, denominator);
}
/**
* Calculate interest paid over a specific period
* @param schedule The amortization schedule
* @param startPeriod Start period (inclusive)
* @param endPeriod End period (inclusive)
*/
public interestForPeriod(
schedule: IAmortizationSchedule,
startPeriod: number,
endPeriod: number
): plugins.Decimal {
return schedule.payments
.filter(p => p.period >= startPeriod && p.period <= endPeriod)
.reduce((sum, p) => this.add(sum, p.interest), this.decimal(0));
}
/**
* Calculate principal paid over a specific period
* @param schedule The amortization schedule
* @param startPeriod Start period (inclusive)
* @param endPeriod End period (inclusive)
*/
public principalForPeriod(
schedule: IAmortizationSchedule,
startPeriod: number,
endPeriod: number
): plugins.Decimal {
return schedule.payments
.filter(p => p.period >= startPeriod && p.period <= endPeriod)
.reduce((sum, p) => this.add(sum, p.principal), this.decimal(0));
}
/**
* Calculate the payoff date with extra payments
* @param options Loan options with extra payment
*/
public payoffDate(options: ILoanOptions, startDate: Date = new Date()): Date {
const schedule = this.schedule(options);
const lastPayment = schedule.payments[schedule.payments.length - 1];
const monthsToPayoff = lastPayment.period;
const payoffDate = new Date(startDate);
payoffDate.setMonth(payoffDate.getMonth() + monthsToPayoff);
return payoffDate;
}
/**
* Calculate interest savings with extra payments
* @param regularOptions Loan options without extra payment
* @param extraPaymentOptions Loan options with extra payment
*/
public interestSavings(
regularOptions: ILoanOptions,
extraPaymentOptions: ILoanOptions
): plugins.Decimal {
const regularSchedule = this.schedule(regularOptions);
const extraSchedule = this.schedule(extraPaymentOptions);
return this.subtract(regularSchedule.totalInterest, extraSchedule.totalInterest);
}
/**
* Calculate biweekly payment amount (26 payments per year)
* @param monthlyPayment The regular monthly payment
*/
public biweeklyPayment(monthlyPayment: plugins.Decimal.Value): plugins.Decimal {
return this.divide(this.decimal(monthlyPayment), 2);
}
/**
* Calculate weekly payment amount (52 payments per year)
* @param monthlyPayment The regular monthly payment
*/
public weeklyPayment(monthlyPayment: plugins.Decimal.Value): plugins.Decimal {
const monthly = this.decimal(monthlyPayment);
const yearlyPayment = this.multiply(monthly, 12);
return this.divide(yearlyPayment, 52);
}
/**
* Calculate the maximum loan amount based on payment capacity
* @param maxPayment Maximum monthly payment
* @param annualRate Annual interest rate
* @param termMonths Term in months
*/
public maxLoanAmount(
maxPayment: plugins.Decimal.Value,
annualRate: plugins.Decimal.Value,
termMonths: plugins.Decimal.Value
): plugins.Decimal {
const payment = this.decimal(maxPayment);
const r = this.divide(this.decimal(annualRate), 12);
const n = this.decimal(termMonths);
if (r.isZero()) {
return this.multiply(payment, n);
}
const onePlusR = this.add(1, r);
const factor = this.power(onePlusR, this.multiply(-1, n));
const numerator = this.subtract(1, factor);
const denominator = this.divide(numerator, r);
return this.multiply(payment, denominator);
}
/**
* Calculate loan-to-value ratio (LTV)
* @param loanAmount The loan amount
* @param propertyValue The property value
*/
public ltv(
loanAmount: plugins.Decimal.Value,
propertyValue: plugins.Decimal.Value
): plugins.Decimal {
const loan = this.decimal(loanAmount);
const value = this.decimal(propertyValue);
if (value.isZero()) {
throw new Error('Property value cannot be zero');
}
return this.divide(loan, value);
}
/**
* Calculate debt-to-income ratio (DTI)
* @param monthlyDebtPayments Total monthly debt payments
* @param monthlyIncome Monthly gross income
*/
public dti(
monthlyDebtPayments: plugins.Decimal.Value,
monthlyIncome: plugins.Decimal.Value
): plugins.Decimal {
const debt = this.decimal(monthlyDebtPayments);
const income = this.decimal(monthlyIncome);
if (income.isZero()) {
throw new Error('Monthly income cannot be zero');
}
return this.divide(debt, income);
}
}

View File

@@ -0,0 +1,178 @@
import * as plugins from './calculation.plugins.js';
export interface ICalculatorOptions {
precision?: number;
rounding?: plugins.Decimal.Rounding;
}
/**
* Base calculator class providing high-precision decimal arithmetic
* for financial calculations
*/
export class Calculator {
private precision: number;
private rounding: plugins.Decimal.Rounding;
constructor(options: ICalculatorOptions = {}) {
this.precision = options.precision || 10;
this.rounding = options.rounding || plugins.Decimal.ROUND_HALF_UP;
// Configure Decimal.js globally for this calculator instance
plugins.Decimal.set({
precision: this.precision,
rounding: this.rounding
});
}
/**
* Create a Decimal instance from a value
*/
public decimal(value: plugins.Decimal.Value): plugins.Decimal {
return new plugins.Decimal(value);
}
/**
* Add two or more numbers with decimal precision
*/
public add(...values: plugins.Decimal.Value[]): plugins.Decimal {
return values.reduce<plugins.Decimal>((sum, value) => {
return sum.add(this.decimal(value));
}, new plugins.Decimal(0));
}
/**
* Subtract numbers with decimal precision
*/
public subtract(minuend: plugins.Decimal.Value, ...subtrahends: plugins.Decimal.Value[]): plugins.Decimal {
let result = new plugins.Decimal(minuend);
for (const subtrahend of subtrahends) {
result = result.sub(subtrahend);
}
return result;
}
/**
* Multiply numbers with decimal precision
*/
public multiply(...values: plugins.Decimal.Value[]): plugins.Decimal {
return values.reduce<plugins.Decimal>((product, value) => {
return product.mul(this.decimal(value));
}, new plugins.Decimal(1));
}
/**
* Divide numbers with decimal precision
*/
public divide(dividend: plugins.Decimal.Value, divisor: plugins.Decimal.Value): plugins.Decimal {
const divisorDecimal = new plugins.Decimal(divisor);
if (divisorDecimal.isZero()) {
throw new Error('Division by zero');
}
return new plugins.Decimal(dividend).div(divisor);
}
/**
* Calculate power with decimal precision
*/
public power(base: plugins.Decimal.Value, exponent: plugins.Decimal.Value): plugins.Decimal {
return new plugins.Decimal(base).pow(exponent);
}
/**
* Calculate square root with decimal precision
*/
public sqrt(value: plugins.Decimal.Value): plugins.Decimal {
return new plugins.Decimal(value).sqrt();
}
/**
* Calculate natural logarithm with decimal precision
*/
public ln(value: plugins.Decimal.Value): plugins.Decimal {
return new plugins.Decimal(value).ln();
}
/**
* Calculate logarithm base 10 with decimal precision
*/
public log10(value: plugins.Decimal.Value): plugins.Decimal {
return new plugins.Decimal(value).log();
}
/**
* Calculate exponential (e^x) with decimal precision
*/
public exp(value: plugins.Decimal.Value): plugins.Decimal {
return new plugins.Decimal(value).exp();
}
/**
* Round a number to specified decimal places
*/
public round(value: plugins.Decimal.Value, decimalPlaces: number = 0): plugins.Decimal {
return new plugins.Decimal(value).toDecimalPlaces(decimalPlaces);
}
/**
* Check if a value equals another value
*/
public equals(value1: plugins.Decimal.Value, value2: plugins.Decimal.Value): boolean {
return new plugins.Decimal(value1).equals(value2);
}
/**
* Check if a value is greater than another value
*/
public greaterThan(value1: plugins.Decimal.Value, value2: plugins.Decimal.Value): boolean {
return new plugins.Decimal(value1).greaterThan(value2);
}
/**
* Check if a value is less than another value
*/
public lessThan(value1: plugins.Decimal.Value, value2: plugins.Decimal.Value): boolean {
return new plugins.Decimal(value1).lessThan(value2);
}
/**
* Get the minimum value from an array of values
*/
public min(...values: plugins.Decimal.Value[]): plugins.Decimal {
return plugins.Decimal.min(...values);
}
/**
* Get the maximum value from an array of values
*/
public max(...values: plugins.Decimal.Value[]): plugins.Decimal {
return plugins.Decimal.max(...values);
}
/**
* Calculate the absolute value
*/
public abs(value: plugins.Decimal.Value): plugins.Decimal {
return new plugins.Decimal(value).abs();
}
/**
* Convert a decimal to a regular number
*/
public toNumber(value: plugins.Decimal.Value): number {
return new plugins.Decimal(value).toNumber();
}
/**
* Convert a decimal to a string
*/
public toString(value: plugins.Decimal.Value): string {
return new plugins.Decimal(value).toString();
}
/**
* Convert a decimal to a fixed-point string
*/
public toFixed(value: plugins.Decimal.Value, decimalPlaces: number = 2): string {
return new plugins.Decimal(value).toFixed(decimalPlaces);
}
}

View File

@@ -0,0 +1,328 @@
import * as plugins from './calculation.plugins.js';
import { Calculator } from './calculation.classes.calculator.js';
export interface ICurrencyOptions {
code: string;
symbol?: string;
decimals?: number;
thousandsSeparator?: string;
decimalSeparator?: string;
symbolPosition?: 'before' | 'after';
spaceBetweenSymbolAndValue?: boolean;
}
export interface IExchangeRate {
from: string;
to: string;
rate: plugins.Decimal.Value;
timestamp?: Date;
}
export interface IMoneyValue {
amount: plugins.Decimal;
currency: string;
}
/**
* Currency calculations and formatting class
*/
export class Currency extends Calculator {
private currencies: Map<string, ICurrencyOptions> = new Map();
private exchangeRates: Map<string, plugins.Decimal> = new Map();
constructor() {
super({ precision: 10 });
this.initializeCommonCurrencies();
}
/**
* Initialize common currencies with their default options
*/
private initializeCommonCurrencies(): void {
const commonCurrencies: ICurrencyOptions[] = [
{ code: 'USD', symbol: '$', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
{ code: 'EUR', symbol: '€', decimals: 2, thousandsSeparator: '.', decimalSeparator: ',', symbolPosition: 'before' },
{ code: 'GBP', symbol: '£', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
{ code: 'JPY', symbol: '¥', decimals: 0, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
{ code: 'CHF', symbol: 'Fr.', decimals: 2, thousandsSeparator: "'", decimalSeparator: '.', symbolPosition: 'after', spaceBetweenSymbolAndValue: true },
{ code: 'CAD', symbol: 'C$', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
{ code: 'AUD', symbol: 'A$', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
{ code: 'CNY', symbol: '¥', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
{ code: 'INR', symbol: '₹', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
{ code: 'BTC', symbol: '₿', decimals: 8, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' }
];
commonCurrencies.forEach(currency => {
this.currencies.set(currency.code, currency);
});
}
/**
* Register a new currency or update existing one
*/
public registerCurrency(options: ICurrencyOptions): void {
this.currencies.set(options.code, {
...options,
decimals: options.decimals ?? 2,
thousandsSeparator: options.thousandsSeparator ?? ',',
decimalSeparator: options.decimalSeparator ?? '.',
symbolPosition: options.symbolPosition ?? 'before',
spaceBetweenSymbolAndValue: options.spaceBetweenSymbolAndValue ?? false
});
}
/**
* Set exchange rate between two currencies
* @param from Source currency code
* @param to Target currency code
* @param rate Exchange rate (how many 'to' units per 'from' unit)
*/
public setExchangeRate(from: string, to: string, rate: plugins.Decimal.Value): void {
const key = `${from}_${to}`;
this.exchangeRates.set(key, this.decimal(rate));
// Also set the inverse rate
const inverseKey = `${to}_${from}`;
const inverseRate = this.divide(1, rate);
this.exchangeRates.set(inverseKey, inverseRate);
}
/**
* Set multiple exchange rates at once
*/
public setExchangeRates(rates: IExchangeRate[]): void {
rates.forEach(rate => {
this.setExchangeRate(rate.from, rate.to, rate.rate);
});
}
/**
* Convert amount from one currency to another
* @param amount The amount to convert
* @param from Source currency code
* @param to Target currency code
*/
public convert(
amount: plugins.Decimal.Value,
from: string,
to: string
): plugins.Decimal {
if (from === to) {
return this.decimal(amount);
}
const key = `${from}_${to}`;
const rate = this.exchangeRates.get(key);
if (!rate) {
throw new Error(`Exchange rate not found for ${from} to ${to}`);
}
return this.multiply(amount, rate);
}
/**
* Create a money value object
*/
public money(amount: plugins.Decimal.Value, currency: string): IMoneyValue {
return {
amount: this.decimal(amount),
currency
};
}
/**
* Add two money values (must be same currency)
*/
public addMoney(money1: IMoneyValue, money2: IMoneyValue): IMoneyValue {
if (money1.currency !== money2.currency) {
throw new Error('Cannot add money values with different currencies');
}
return {
amount: this.add(money1.amount, money2.amount),
currency: money1.currency
};
}
/**
* Subtract two money values (must be same currency)
*/
public subtractMoney(money1: IMoneyValue, money2: IMoneyValue): IMoneyValue {
if (money1.currency !== money2.currency) {
throw new Error('Cannot subtract money values with different currencies');
}
return {
amount: this.subtract(money1.amount, money2.amount),
currency: money1.currency
};
}
/**
* Format currency value for display
* @param amount The amount to format
* @param currencyCode The currency code
* @param options Override formatting options
*/
public format(
amount: plugins.Decimal.Value,
currencyCode: string,
options?: Partial<ICurrencyOptions>
): string {
const currency = this.currencies.get(currencyCode);
if (!currency) {
throw new Error(`Currency ${currencyCode} not registered`);
}
const formatOptions = { ...currency, ...options };
const value = this.decimal(amount);
// Round to the correct number of decimal places
const rounded = this.round(value, formatOptions.decimals || 0);
// Convert to string with fixed decimals
let numberStr = this.toFixed(rounded, formatOptions.decimals || 0);
// Split into integer and decimal parts
const parts = numberStr.split('.');
// Apply thousands separator to integer part
if (formatOptions.thousandsSeparator) {
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, formatOptions.thousandsSeparator);
}
// Join parts with appropriate decimal separator
if (parts.length === 2) {
numberStr = parts.join(formatOptions.decimalSeparator || '.');
} else {
numberStr = parts[0];
}
// Add currency symbol
if (formatOptions.symbol) {
const space = formatOptions.spaceBetweenSymbolAndValue ? ' ' : '';
if (formatOptions.symbolPosition === 'after') {
return `${numberStr}${space}${formatOptions.symbol}`;
} else {
return `${formatOptions.symbol}${space}${numberStr}`;
}
}
return numberStr;
}
/**
* Parse a formatted currency string back to a decimal value
* @param formattedValue The formatted string
* @param currencyCode The currency code
*/
public parse(formattedValue: string, currencyCode: string): plugins.Decimal {
const currency = this.currencies.get(currencyCode);
if (!currency) {
throw new Error(`Currency ${currencyCode} not registered`);
}
// Remove currency symbol
let cleanValue = formattedValue;
if (currency.symbol) {
cleanValue = cleanValue.replace(currency.symbol, '');
}
// Remove thousands separator
if (currency.thousandsSeparator) {
cleanValue = cleanValue.replace(new RegExp(`\\${currency.thousandsSeparator}`, 'g'), '');
}
// Replace decimal separator with standard dot
if (currency.decimalSeparator && currency.decimalSeparator !== '.') {
cleanValue = cleanValue.replace(currency.decimalSeparator, '.');
}
// Remove any remaining spaces and parse
cleanValue = cleanValue.trim();
return this.decimal(cleanValue);
}
/**
* Round amount to currency's standard decimal places
*/
public roundToCurrency(amount: plugins.Decimal.Value, currencyCode: string): plugins.Decimal {
const currency = this.currencies.get(currencyCode);
if (!currency) {
throw new Error(`Currency ${currencyCode} not registered`);
}
return this.round(amount, currency.decimals || 0);
}
/**
* Calculate percentage of amount
* @param amount The base amount
* @param percentage The percentage (e.g., 15 for 15%)
*/
public percentage(amount: plugins.Decimal.Value, percentage: plugins.Decimal.Value): plugins.Decimal {
const percent = this.divide(percentage, 100);
return this.multiply(amount, percent);
}
/**
* Calculate markup amount
* @param cost The base cost
* @param markupPercentage The markup percentage
*/
public markup(cost: plugins.Decimal.Value, markupPercentage: plugins.Decimal.Value): plugins.Decimal {
const markupAmount = this.percentage(cost, markupPercentage);
return this.add(cost, markupAmount);
}
/**
* Calculate discount amount
* @param price The original price
* @param discountPercentage The discount percentage
*/
public discount(price: plugins.Decimal.Value, discountPercentage: plugins.Decimal.Value): plugins.Decimal {
const discountAmount = this.percentage(price, discountPercentage);
return this.subtract(price, discountAmount);
}
/**
* Calculate tax amount
* @param amount The base amount
* @param taxRate The tax rate (e.g., 0.08 for 8%)
*/
public tax(amount: plugins.Decimal.Value, taxRate: plugins.Decimal.Value): plugins.Decimal {
return this.multiply(amount, taxRate);
}
/**
* Calculate total with tax
* @param amount The base amount
* @param taxRate The tax rate
*/
public withTax(amount: plugins.Decimal.Value, taxRate: plugins.Decimal.Value): plugins.Decimal {
const taxAmount = this.tax(amount, taxRate);
return this.add(amount, taxAmount);
}
/**
* Extract base amount from a tax-inclusive price
* @param totalAmount The total amount including tax
* @param taxRate The tax rate
*/
public extractTax(totalAmount: plugins.Decimal.Value, taxRate: plugins.Decimal.Value): {
baseAmount: plugins.Decimal;
taxAmount: plugins.Decimal;
} {
const divisor = this.add(1, taxRate);
const baseAmount = this.divide(totalAmount, divisor);
const taxAmount = this.subtract(totalAmount, baseAmount);
return { baseAmount, taxAmount };
}
}

View File

@@ -0,0 +1,325 @@
import * as plugins from './calculation.plugins.js';
import { Calculator } from './calculation.classes.calculator.js';
export interface ICashFlow {
amount: plugins.Decimal.Value;
period?: number;
}
/**
* Financial calculations class providing time value of money and investment analysis functions
*/
export class Financial extends Calculator {
constructor() {
super({ precision: 15 });
}
/**
* Calculate Present Value (PV)
* @param futureValue The future value
* @param rate The interest rate per period
* @param periods The number of periods
*/
public presentValue(
futureValue: plugins.Decimal.Value,
rate: plugins.Decimal.Value,
periods: plugins.Decimal.Value
): plugins.Decimal {
const r = this.decimal(rate);
const n = this.decimal(periods);
const fv = this.decimal(futureValue);
const denominator = this.power(this.add(1, r), n);
return this.divide(fv, denominator);
}
/**
* Calculate Future Value (FV)
* @param presentValue The present value
* @param rate The interest rate per period
* @param periods The number of periods
*/
public futureValue(
presentValue: plugins.Decimal.Value,
rate: plugins.Decimal.Value,
periods: plugins.Decimal.Value
): plugins.Decimal {
const r = this.decimal(rate);
const n = this.decimal(periods);
const pv = this.decimal(presentValue);
const multiplier = this.power(this.add(1, r), n);
return this.multiply(pv, multiplier);
}
/**
* Calculate Payment (PMT) for a loan
* @param principal The loan principal
* @param rate The interest rate per period
* @param periods The number of periods
*/
public payment(
principal: plugins.Decimal.Value,
rate: plugins.Decimal.Value,
periods: plugins.Decimal.Value
): plugins.Decimal {
const r = this.decimal(rate);
const n = this.decimal(periods);
const p = this.decimal(principal);
if (r.isZero()) {
return this.divide(p, n);
}
const numerator = this.multiply(p, r);
const denominator = this.subtract(1, this.power(this.add(1, r), this.multiply(-1, n)));
return this.divide(numerator, denominator);
}
/**
* Calculate Net Present Value (NPV)
* @param rate The discount rate
* @param cashFlows Array of cash flows (first element is initial investment, usually negative)
*/
public npv(rate: plugins.Decimal.Value, cashFlows: plugins.Decimal.Value[]): plugins.Decimal {
const r = this.decimal(rate);
return cashFlows.reduce<plugins.Decimal>((npv, cashFlow, period) => {
const cf = this.decimal(cashFlow);
const discountFactor = this.power(this.add(1, r), period);
const presentValue = this.divide(cf, discountFactor);
return this.add(npv, presentValue);
}, this.decimal(0));
}
/**
* Calculate Internal Rate of Return (IRR) using Newton-Raphson method
* @param cashFlows Array of cash flows
* @param guess Initial guess for IRR (default: 0.1)
* @param tolerance Convergence tolerance (default: 0.000001)
* @param maxIterations Maximum iterations (default: 1000)
*/
public irr(
cashFlows: plugins.Decimal.Value[],
guess: plugins.Decimal.Value = 0.1,
tolerance: plugins.Decimal.Value = 0.000001,
maxIterations: number = 1000
): plugins.Decimal {
let rate = this.decimal(guess);
const tol = this.decimal(tolerance);
for (let i = 0; i < maxIterations; i++) {
const npvValue = this.npv(rate, cashFlows);
const npvDerivative = this.npvDerivative(rate, cashFlows);
if (npvDerivative.isZero()) {
throw new Error('IRR calculation failed: derivative is zero');
}
const newRate = this.subtract(rate, this.divide(npvValue, npvDerivative));
if (this.abs(this.subtract(newRate, rate)).lessThan(tol)) {
return newRate;
}
rate = newRate;
}
throw new Error('IRR calculation failed: maximum iterations reached');
}
/**
* Calculate the derivative of NPV with respect to rate (for IRR calculation)
*/
private npvDerivative(rate: plugins.Decimal.Value, cashFlows: plugins.Decimal.Value[]): plugins.Decimal {
const r = this.decimal(rate);
return cashFlows.reduce<plugins.Decimal>((derivative, cashFlow, period) => {
if (period === 0) return derivative;
const cf = this.decimal(cashFlow);
const negPeriod = this.decimal(period).neg();
const onePlusR = this.add(1, r);
const power = this.add(negPeriod, -1);
const discountFactor = this.power(onePlusR, power);
const termDerivative = this.multiply(this.multiply(negPeriod, cf), discountFactor);
return this.add(derivative, termDerivative);
}, this.decimal(0));
}
/**
* Calculate Modified Internal Rate of Return (MIRR)
* @param cashFlows Array of cash flows
* @param financeRate Rate for negative cash flows
* @param reinvestRate Rate for positive cash flows
*/
public mirr(
cashFlows: plugins.Decimal.Value[],
financeRate: plugins.Decimal.Value,
reinvestRate: plugins.Decimal.Value
): plugins.Decimal {
const fRate = this.decimal(financeRate);
const rRate = this.decimal(reinvestRate);
const n = cashFlows.length - 1;
let pvNegative = this.decimal(0);
let fvPositive = this.decimal(0);
cashFlows.forEach((cashFlow, period) => {
const cf = this.decimal(cashFlow);
if (cf.lessThan(0)) {
pvNegative = this.add(pvNegative, this.presentValue(cf, fRate, period));
} else {
fvPositive = this.add(fvPositive, this.futureValue(cf, rRate, n - period));
}
});
if (pvNegative.isZero() || fvPositive.isZero()) {
throw new Error('MIRR calculation failed: no negative or positive cash flows');
}
const ratio = this.divide(fvPositive, this.abs(pvNegative));
const result = this.subtract(this.power(ratio, this.divide(1, n)), 1);
return result;
}
/**
* Calculate the number of periods for an investment to reach a target value
* @param presentValue The present value
* @param futureValue The future value
* @param rate The interest rate per period
*/
public periods(
presentValue: plugins.Decimal.Value,
futureValue: plugins.Decimal.Value,
rate: plugins.Decimal.Value
): plugins.Decimal {
const pv = this.decimal(presentValue);
const fv = this.decimal(futureValue);
const r = this.decimal(rate);
if (r.isZero()) {
throw new Error('Cannot calculate periods with zero interest rate');
}
const ratio = this.divide(fv, pv);
const numerator = this.ln(ratio);
const denominator = this.ln(this.add(1, r));
return this.divide(numerator, denominator);
}
/**
* Calculate the interest rate given PV, FV, and periods
* @param presentValue The present value
* @param futureValue The future value
* @param periods The number of periods
*/
public rate(
presentValue: plugins.Decimal.Value,
futureValue: plugins.Decimal.Value,
periods: plugins.Decimal.Value
): plugins.Decimal {
const pv = this.decimal(presentValue);
const fv = this.decimal(futureValue);
const n = this.decimal(periods);
if (n.isZero()) {
throw new Error('Cannot calculate rate with zero periods');
}
const ratio = this.divide(fv, pv);
const exponent = this.divide(1, n);
const result = this.subtract(this.power(ratio, exponent), 1);
return result;
}
/**
* Calculate Extended Internal Rate of Return (XIRR) for irregular cash flows
* @param cashFlows Array of cash flow objects with amounts and dates
* @param dates Array of dates corresponding to cash flows
* @param guess Initial guess for XIRR (default: 0.1)
*/
public xirr(
cashFlows: plugins.Decimal.Value[],
dates: Date[],
guess: plugins.Decimal.Value = 0.1
): plugins.Decimal {
if (cashFlows.length !== dates.length) {
throw new Error('Cash flows and dates arrays must have the same length');
}
const startDate = dates[0];
const yearFractions = dates.map(date => {
const daysDiff = (date.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24);
return this.divide(daysDiff, 365);
});
let rate = this.decimal(guess);
const tolerance = this.decimal(0.000001);
const maxIterations = 1000;
for (let i = 0; i < maxIterations; i++) {
const xnpv = this.xnpv(rate, cashFlows, yearFractions);
const xnpvDerivative = this.xnpvDerivative(rate, cashFlows, yearFractions);
if (xnpvDerivative.isZero()) {
throw new Error('XIRR calculation failed: derivative is zero');
}
const newRate = this.subtract(rate, this.divide(xnpv, xnpvDerivative));
if (this.abs(this.subtract(newRate, rate)).lessThan(tolerance)) {
return newRate;
}
rate = newRate;
}
throw new Error('XIRR calculation failed: maximum iterations reached');
}
/**
* Calculate XNPV for irregular cash flows
*/
private xnpv(
rate: plugins.Decimal.Value,
cashFlows: plugins.Decimal.Value[],
yearFractions: plugins.Decimal[]
): plugins.Decimal {
const r = this.decimal(rate);
return cashFlows.reduce<plugins.Decimal>((npv, cashFlow, index) => {
const cf = this.decimal(cashFlow);
const t = yearFractions[index];
const discountFactor = this.power(this.add(1, r), t);
const presentValue = this.divide(cf, discountFactor);
return this.add(npv, presentValue);
}, this.decimal(0));
}
/**
* Calculate the derivative of XNPV
*/
private xnpvDerivative(
rate: plugins.Decimal.Value,
cashFlows: plugins.Decimal.Value[],
yearFractions: plugins.Decimal[]
): plugins.Decimal {
const r = this.decimal(rate);
return cashFlows.reduce<plugins.Decimal>((derivative, cashFlow, index) => {
const cf = this.decimal(cashFlow);
const t = yearFractions[index];
const negT = this.multiply(-1, t);
const discountFactor = this.power(this.add(1, r), this.subtract(negT, 1));
const termDerivative = this.multiply(this.multiply(negT, cf), discountFactor);
return this.add(derivative, termDerivative);
}, this.decimal(0));
}
}

View File

@@ -0,0 +1,297 @@
import * as plugins from './calculation.plugins.js';
import { Calculator } from './calculation.classes.calculator.js';
export type CompoundingFrequency = 'annually' | 'semiannually' | 'quarterly' | 'monthly' | 'weekly' | 'daily' | 'continuous';
export interface IInterestOptions {
principal: plugins.Decimal.Value;
rate: plugins.Decimal.Value;
time: plugins.Decimal.Value;
compoundingFrequency?: CompoundingFrequency;
}
/**
* Interest calculations class providing various interest computation methods
*/
export class Interest extends Calculator {
private readonly frequencyMap: Record<CompoundingFrequency, number> = {
annually: 1,
semiannually: 2,
quarterly: 4,
monthly: 12,
weekly: 52,
daily: 365,
continuous: 0
};
constructor() {
super({ precision: 15 });
}
/**
* Calculate simple interest
* @param principal The principal amount
* @param rate The annual interest rate (as decimal, e.g., 0.05 for 5%)
* @param time The time period in years
*/
public simple(
principal: plugins.Decimal.Value,
rate: plugins.Decimal.Value,
time: plugins.Decimal.Value
): plugins.Decimal {
const p = this.decimal(principal);
const r = this.decimal(rate);
const t = this.decimal(time);
return this.multiply(p, r, t);
}
/**
* Calculate simple interest amount (principal + interest)
*/
public simpleAmount(
principal: plugins.Decimal.Value,
rate: plugins.Decimal.Value,
time: plugins.Decimal.Value
): plugins.Decimal {
const p = this.decimal(principal);
const interest = this.simple(principal, rate, time);
return this.add(p, interest);
}
/**
* Calculate compound interest
* @param principal The principal amount
* @param rate The annual interest rate (as decimal)
* @param time The time period in years
* @param frequency The compounding frequency (default: annually)
*/
public compound(
principal: plugins.Decimal.Value,
rate: plugins.Decimal.Value,
time: plugins.Decimal.Value,
frequency: CompoundingFrequency = 'annually'
): plugins.Decimal {
const p = this.decimal(principal);
const r = this.decimal(rate);
const t = this.decimal(time);
if (frequency === 'continuous') {
return this.continuousCompound(principal, rate, time);
}
const n = this.decimal(this.frequencyMap[frequency]);
const amount = this.compoundAmount(principal, rate, time, frequency);
return this.subtract(amount, p);
}
/**
* Calculate compound interest amount (principal + interest)
*/
public compoundAmount(
principal: plugins.Decimal.Value,
rate: plugins.Decimal.Value,
time: plugins.Decimal.Value,
frequency: CompoundingFrequency = 'annually'
): plugins.Decimal {
const p = this.decimal(principal);
const r = this.decimal(rate);
const t = this.decimal(time);
if (frequency === 'continuous') {
return this.continuousCompoundAmount(principal, rate, time);
}
const n = this.decimal(this.frequencyMap[frequency]);
const ratePerPeriod = this.divide(r, n);
const periods = this.multiply(n, t);
const multiplier = this.power(this.add(1, ratePerPeriod), periods);
return this.multiply(p, multiplier);
}
/**
* Calculate continuous compound interest
*/
private continuousCompound(
principal: plugins.Decimal.Value,
rate: plugins.Decimal.Value,
time: plugins.Decimal.Value
): plugins.Decimal {
const p = this.decimal(principal);
const amount = this.continuousCompoundAmount(principal, rate, time);
return this.subtract(amount, p);
}
/**
* Calculate continuous compound interest amount
*/
private continuousCompoundAmount(
principal: plugins.Decimal.Value,
rate: plugins.Decimal.Value,
time: plugins.Decimal.Value
): plugins.Decimal {
const p = this.decimal(principal);
const r = this.decimal(rate);
const t = this.decimal(time);
const exponent = this.multiply(r, t);
const multiplier = this.exp(exponent);
return this.multiply(p, multiplier);
}
/**
* Calculate effective annual rate (EAR) from nominal rate
* @param nominalRate The nominal annual interest rate
* @param frequency The compounding frequency
*/
public effectiveAnnualRate(
nominalRate: plugins.Decimal.Value,
frequency: CompoundingFrequency = 'annually'
): plugins.Decimal {
const r = this.decimal(nominalRate);
if (frequency === 'continuous') {
return this.subtract(this.exp(r), 1);
}
const n = this.decimal(this.frequencyMap[frequency]);
const ratePerPeriod = this.divide(r, n);
const onePlusRate = this.add(1, ratePerPeriod);
const compounded = this.power(onePlusRate, n);
return this.subtract(compounded, 1);
}
/**
* Calculate nominal rate from effective annual rate
* @param effectiveRate The effective annual rate
* @param frequency The compounding frequency
*/
public nominalRate(
effectiveRate: plugins.Decimal.Value,
frequency: CompoundingFrequency = 'annually'
): plugins.Decimal {
const ear = this.decimal(effectiveRate);
if (frequency === 'continuous') {
return this.ln(this.add(1, ear));
}
const n = this.decimal(this.frequencyMap[frequency]);
const onePlusEar = this.add(1, ear);
const exponent = this.divide(1, n);
const nthRoot = this.power(onePlusEar, exponent);
const ratePerPeriod = this.subtract(nthRoot, 1);
return this.multiply(ratePerPeriod, n);
}
/**
* Calculate the real interest rate adjusted for inflation
* @param nominalRate The nominal interest rate
* @param inflationRate The inflation rate
*/
public realRate(
nominalRate: plugins.Decimal.Value,
inflationRate: plugins.Decimal.Value
): plugins.Decimal {
const r = this.decimal(nominalRate);
const i = this.decimal(inflationRate);
const numerator = this.subtract(r, i);
const denominator = this.add(1, i);
return this.divide(numerator, denominator);
}
/**
* Calculate the time required to double an investment
* @param rate The interest rate
* @param frequency The compounding frequency
*/
public doubleTime(
rate: plugins.Decimal.Value,
frequency: CompoundingFrequency = 'annually'
): plugins.Decimal {
const r = this.decimal(rate);
if (r.isZero()) {
throw new Error('Cannot calculate doubling time with zero interest rate');
}
if (frequency === 'continuous') {
return this.divide(this.ln(2), r);
}
const n = this.decimal(this.frequencyMap[frequency]);
const ratePerPeriod = this.divide(r, n);
const numerator = this.ln(2);
const denominator = this.multiply(n, this.ln(this.add(1, ratePerPeriod)));
return this.divide(numerator, denominator);
}
/**
* Calculate the Rule of 72 approximation for doubling time
* @param rate The annual interest rate (as percentage, e.g., 5 for 5%)
*/
public ruleOf72(rate: plugins.Decimal.Value): plugins.Decimal {
const r = this.decimal(rate);
if (r.isZero()) {
throw new Error('Cannot calculate Rule of 72 with zero interest rate');
}
return this.divide(72, r);
}
/**
* Calculate periodic interest rate from annual rate
* @param annualRate The annual interest rate
* @param frequency The compounding frequency
*/
public periodicRate(
annualRate: plugins.Decimal.Value,
frequency: CompoundingFrequency
): plugins.Decimal {
if (frequency === 'continuous') {
throw new Error('Periodic rate not applicable for continuous compounding');
}
const r = this.decimal(annualRate);
const n = this.decimal(this.frequencyMap[frequency]);
return this.divide(r, n);
}
/**
* Calculate annual percentage yield (APY)
* Same as effective annual rate but commonly used in banking
*/
public apy(
nominalRate: plugins.Decimal.Value,
frequency: CompoundingFrequency = 'annually'
): plugins.Decimal {
return this.effectiveAnnualRate(nominalRate, frequency);
}
/**
* Calculate interest on interest (compound interest minus simple interest)
*/
public interestOnInterest(
principal: plugins.Decimal.Value,
rate: plugins.Decimal.Value,
time: plugins.Decimal.Value,
frequency: CompoundingFrequency = 'annually'
): plugins.Decimal {
const simpleInt = this.simple(principal, rate, time);
const compoundInt = this.compound(principal, rate, time, frequency);
return this.subtract(compoundInt, simpleInt);
}
}

View File

@@ -0,0 +1,7 @@
import * as smartpromise from '@push.rocks/smartpromise';
import { Decimal } from 'decimal.js';
export {
smartpromise,
Decimal
};

5
ts/index.ts Normal file
View File

@@ -0,0 +1,5 @@
export * from './calculation.classes.calculator.js';
export * from './calculation.classes.financial.js';
export * from './calculation.classes.interest.js';
export * from './calculation.classes.amortization.js';
export * from './calculation.classes.currency.js';