UNPKG

hyperformula

Version:

HyperFormula is a JavaScript engine for efficient processing of spreadsheet-like data and formulas

835 lines 25.3 kB
/** * @license * Copyright (c) 2025 Handsoncode. All rights reserved. */ import { CellError, ErrorType } from "../../Cell.mjs"; import { ErrorMessage } from "../../error-message.mjs"; import { EmptyValue, getRawValue, isExtendedNumber, NumberType } from "../InterpreterValue.mjs"; import { FunctionArgumentType, FunctionPlugin } from "./FunctionPlugin.mjs"; export class FinancialPlugin extends FunctionPlugin { pmt(ast, state) { return this.runFunction(ast.args, state, this.metadata('PMT'), pmtCore); } ipmt(ast, state) { return this.runFunction(ast.args, state, this.metadata('IPMT'), ipmtCore); } ppmt(ast, state) { return this.runFunction(ast.args, state, this.metadata('PPMT'), ppmtCore); } fv(ast, state) { return this.runFunction(ast.args, state, this.metadata('FV'), fvCore); } cumipmt(ast, state) { return this.runFunction(ast.args, state, this.metadata('CUMIPMT'), (rate, periods, value, start, end, type) => { if (start > end) { return new CellError(ErrorType.NUM, ErrorMessage.EndStartPeriod); } let acc = 0; for (let i = start; i <= end; i++) { acc += ipmtCore(rate, i, periods, value, 0, type); } return acc; }); } cumprinc(ast, state) { return this.runFunction(ast.args, state, this.metadata('CUMPRINC'), (rate, periods, value, start, end, type) => { if (start > end) { return new CellError(ErrorType.NUM, ErrorMessage.EndStartPeriod); } let acc = 0; for (let i = start; i <= end; i++) { acc += ppmtCore(rate, i, periods, value, 0, type); } return acc; }); } db(ast, state) { return this.runFunction(ast.args, state, this.metadata('DB'), (cost, salvage, life, period, month) => { if (month === 12 && period > life || period > life + 1) { return new CellError(ErrorType.NUM, ErrorMessage.PeriodLong); } if (salvage >= cost) { return 0; } const rate = Math.round((1 - Math.pow(salvage / cost, 1 / life)) * 1000) / 1000; const initial = cost * rate * month / 12; if (period === 1) { return initial; } let total = initial; for (let i = 0; i < period - 2; i++) { total += (cost - total) * rate; } if (period === life + 1) { return (cost - total) * rate * (12 - month) / 12; } return (cost - total) * rate; }); } ddb(ast, state) { return this.runFunction(ast.args, state, this.metadata('DDB'), (cost, salvage, life, period, factor) => { if (period > life) { return new CellError(ErrorType.NUM); } let rate = factor / life; let oldValue; if (rate >= 1) { rate = 1; if (period === 1) { oldValue = cost; } else { oldValue = 0; } } else { oldValue = cost * Math.pow(1 - rate, period - 1); } const newValue = cost * Math.pow(1 - rate, period); return Math.max(oldValue - Math.max(salvage, newValue), 0); }); } dollarde(ast, state) { return this.runFunction(ast.args, state, this.metadata('DOLLARDE'), (dollar, fraction) => { if (fraction < 1) { return new CellError(ErrorType.DIV_BY_ZERO); } fraction = Math.trunc(fraction); while (fraction > 10) { fraction /= 10; } return Math.trunc(dollar) + (dollar - Math.trunc(dollar)) * 10 / fraction; }); } dollarfr(ast, state) { return this.runFunction(ast.args, state, this.metadata('DOLLARFR'), (dollar, fraction) => { if (fraction < 1) { return new CellError(ErrorType.DIV_BY_ZERO); } fraction = Math.trunc(fraction); while (fraction > 10) { fraction /= 10; } return Math.trunc(dollar) + (dollar - Math.trunc(dollar)) * fraction / 10; }); } effect(ast, state) { return this.runFunction(ast.args, state, this.metadata('EFFECT'), (rate, periods) => { periods = Math.trunc(periods); return Math.pow(1 + rate / periods, periods) - 1; }); } ispmt(ast, state) { return this.runFunction(ast.args, state, this.metadata('ISPMT'), (rate, period, periods, value) => { if (periods === 0) { return new CellError(ErrorType.DIV_BY_ZERO); } return value * rate * (period / periods - 1); }); } nominal(ast, state) { return this.runFunction(ast.args, state, this.metadata('NOMINAL'), (rate, periods) => { periods = Math.trunc(periods); return (Math.pow(rate + 1, 1 / periods) - 1) * periods; }); } nper(ast, state) { return this.runFunction(ast.args, state, this.metadata('NPER'), (rate, payment, present, future, type) => { if (rate === 0) { if (payment === 0) { return new CellError(ErrorType.DIV_BY_ZERO); } return (-present - future) / payment; } if (type) { payment *= 1 + rate; } return Math.log((payment - future * rate) / (present * rate + payment)) / Math.log(1 + rate); }); } rate(ast, state) { // Newton's method: https://en.wikipedia.org/wiki/Newton%27s_method return this.runFunction(ast.args, state, this.metadata('RATE'), (periods, payment, present, future, type, guess) => { if (guess <= -1) { return new CellError(ErrorType.VALUE); } const epsMax = 1e-7; const iterMax = 50; let rate = guess; type = type ? 1 : 0; for (let i = 0; i < iterMax; i++) { if (rate <= -1) { return new CellError(ErrorType.NUM); } let y; if (Math.abs(rate) < epsMax) { y = present * (1 + periods * rate) + payment * (1 + rate * type) * periods + future; } else { const f = Math.pow(1 + rate, periods); y = present * f + payment * (1 / rate + type) * (f - 1) + future; } if (Math.abs(y) < epsMax) { return rate; } let dy; if (Math.abs(rate) < epsMax) { dy = present * periods + payment * type * periods; } else { const f = Math.pow(1 + rate, periods); const df = periods * Math.pow(1 + rate, periods - 1); dy = present * df + payment * (1 / rate + type) * df + payment * (-1 / (rate * rate)) * (f - 1); } rate -= y / dy; } return new CellError(ErrorType.NUM); }); } pv(ast, state) { return this.runFunction(ast.args, state, this.metadata('PV'), (rate, periods, payment, future, type) => { type = type ? 1 : 0; if (rate === -1) { if (periods === 0) { return new CellError(ErrorType.NUM); } else { return new CellError(ErrorType.DIV_BY_ZERO); } } if (rate === 0) { return -payment * periods - future; } else { return ((1 - Math.pow(1 + rate, periods)) * payment * (1 + rate * type) / rate - future) / Math.pow(1 + rate, periods); } }); } rri(ast, state) { return this.runFunction(ast.args, state, this.metadata('RRI'), (periods, present, future) => { if (present === 0 || future < 0 && present > 0 || future > 0 && present < 0) { return new CellError(ErrorType.NUM); } return Math.pow(future / present, 1 / periods) - 1; }); } sln(ast, state) { return this.runFunction(ast.args, state, this.metadata('SLN'), (cost, salvage, life) => { if (life === 0) { return new CellError(ErrorType.DIV_BY_ZERO); } return (cost - salvage) / life; }); } syd(ast, state) { return this.runFunction(ast.args, state, this.metadata('SYD'), (cost, salvage, life, period) => { if (period > life) { return new CellError(ErrorType.NUM); } return (cost - salvage) * (life - period + 1) * 2 / (life * (life + 1)); }); } tbilleq(ast, state) { return this.runFunction(ast.args, state, this.metadata('TBILLEQ'), (settlement, maturity, discount) => { settlement = Math.round(settlement); maturity = Math.round(maturity); if (settlement >= maturity) { return new CellError(ErrorType.NUM); } const startDate = this.dateTimeHelper.numberToSimpleDate(settlement); const endDate = this.dateTimeHelper.numberToSimpleDate(maturity); if (endDate.year > startDate.year + 1 || endDate.year === startDate.year + 1 && (endDate.month > startDate.month || endDate.month === startDate.month && endDate.day > startDate.day)) { return new CellError(ErrorType.NUM); } const denom = 360 - discount * (maturity - settlement); if (denom === 0) { return 0; } if (denom < 0) { return new CellError(ErrorType.NUM); } return 365 * discount / denom; }); } tbillprice(ast, state) { return this.runFunction(ast.args, state, this.metadata('TBILLPRICE'), (settlement, maturity, discount) => { settlement = Math.round(settlement); maturity = Math.round(maturity); if (settlement >= maturity) { return new CellError(ErrorType.NUM); } const startDate = this.dateTimeHelper.numberToSimpleDate(settlement); const endDate = this.dateTimeHelper.numberToSimpleDate(maturity); if (endDate.year > startDate.year + 1 || endDate.year === startDate.year + 1 && (endDate.month > startDate.month || endDate.month === startDate.month && endDate.day > startDate.day)) { return new CellError(ErrorType.NUM); } const denom = 360 - discount * (maturity - settlement); if (denom === 0) { return 0; } if (denom < 0) { return new CellError(ErrorType.NUM); } return 100 * (1 - discount * (maturity - settlement) / 360); }); } tbillyield(ast, state) { return this.runFunction(ast.args, state, this.metadata('TBILLYIELD'), (settlement, maturity, price) => { settlement = Math.round(settlement); maturity = Math.round(maturity); if (settlement >= maturity) { return new CellError(ErrorType.NUM); } const startDate = this.dateTimeHelper.numberToSimpleDate(settlement); const endDate = this.dateTimeHelper.numberToSimpleDate(maturity); if (endDate.year > startDate.year + 1 || endDate.year === startDate.year + 1 && (endDate.month > startDate.month || endDate.month === startDate.month && endDate.day > startDate.day)) { return new CellError(ErrorType.NUM); } return (100 - price) * 360 / (price * (maturity - settlement)); }); } fvschedule(ast, state) { return this.runFunction(ast.args, state, this.metadata('FVSCHEDULE'), (value, ratios) => { const vals = ratios.valuesFromTopLeftCorner(); for (const val of vals) { if (val instanceof CellError) { return val; } } for (const val of vals) { if (isExtendedNumber(val)) { value *= 1 + getRawValue(val); } else if (val !== EmptyValue) { return new CellError(ErrorType.VALUE, ErrorMessage.NumberExpected); } } return value; }); } npv(ast, state) { return this.runFunction(ast.args, state, this.metadata('NPV'), (rate, ...args) => { const coerced = this.arithmeticHelper.coerceNumbersExactRanges(args); if (coerced instanceof CellError) { return coerced; } return npvCore(rate, coerced); }); } mirr(ast, state) { return this.runFunction(ast.args, state, this.metadata('MIRR'), (range, frate, rrate) => { const vals = this.arithmeticHelper.manyToExactNumbers(range.valuesFromTopLeftCorner()); if (vals instanceof CellError) { return vals; } let posFlag = false; let negFlag = false; const posValues = []; const negValues = []; for (const val of vals) { if (val > 0) { posFlag = true; posValues.push(val); negValues.push(0); } else if (val < 0) { negFlag = true; negValues.push(val); posValues.push(0); } else { negValues.push(0); posValues.push(0); } } if (!posFlag || !negFlag) { return new CellError(ErrorType.DIV_BY_ZERO); } const n = vals.length; const nom = npvCore(rrate, posValues); if (nom instanceof CellError) { return nom; } const denom = npvCore(frate, negValues); if (denom instanceof CellError) { return denom; } return Math.pow(-nom * Math.pow(1 + rrate, n) / denom / (1 + frate), 1 / (n - 1)) - 1; }); } pduration(ast, state) { return this.runFunction(ast.args, state, this.metadata('PDURATION'), (rate, pv, fv) => (Math.log(fv) - Math.log(pv)) / Math.log(1 + rate)); } xnpv(ast, state) { return this.runFunction(ast.args, state, this.metadata('XNPV'), (rate, values, dates) => { const valArr = values.valuesFromTopLeftCorner(); for (const val of valArr) { if (typeof val !== 'number') { return new CellError(ErrorType.VALUE, ErrorMessage.NumberExpected); } } const valArrNum = valArr; const dateArr = dates.valuesFromTopLeftCorner(); for (const date of dateArr) { if (typeof date !== 'number') { return new CellError(ErrorType.VALUE, ErrorMessage.NumberExpected); } } const dateArrNum = dateArr; if (dateArrNum.length !== valArrNum.length) { return new CellError(ErrorType.NUM, ErrorMessage.EqualLength); } const n = dateArrNum.length; let ret = 0; if (dateArrNum[0] < 0) { return new CellError(ErrorType.NUM, ErrorMessage.ValueSmall); } for (let i = 0; i < n; i++) { dateArrNum[i] = Math.floor(dateArrNum[i]); if (dateArrNum[i] < dateArrNum[0]) { return new CellError(ErrorType.NUM, ErrorMessage.ValueSmall); } ret += valArrNum[i] / Math.pow(1 + rate, (dateArrNum[i] - dateArrNum[0]) / 365); } return ret; }); } } FinancialPlugin.implementedFunctions = { 'PMT': { method: 'pmt', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'IPMT': { method: 'ipmt', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'PPMT': { method: 'ppmt', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'FV': { method: 'fv', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'CUMIPMT': { method: 'cumipmt', parameters: [{ argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }, { argumentType: FunctionArgumentType.INTEGER, minValue: 1 }, { argumentType: FunctionArgumentType.INTEGER, minValue: 1 }, { argumentType: FunctionArgumentType.INTEGER, minValue: 0, maxValue: 1 }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'CUMPRINC': { method: 'cumprinc', parameters: [{ argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }, { argumentType: FunctionArgumentType.INTEGER, minValue: 1 }, { argumentType: FunctionArgumentType.INTEGER, minValue: 1 }, { argumentType: FunctionArgumentType.INTEGER, minValue: 0, maxValue: 1 }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'DB': { method: 'db', parameters: [{ argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.INTEGER, minValue: 0 }, { argumentType: FunctionArgumentType.INTEGER, minValue: 0 }, { argumentType: FunctionArgumentType.INTEGER, minValue: 1, maxValue: 12, defaultValue: 12 }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'DDB': { method: 'ddb', parameters: [{ argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.INTEGER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0, defaultValue: 2 }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'DOLLARDE': { method: 'dollarde', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER, minValue: 0 }] }, 'DOLLARFR': { method: 'dollarfr', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER, minValue: 0 }] }, 'EFFECT': { method: 'effect', parameters: [{ argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, minValue: 1 }], returnNumberType: NumberType.NUMBER_PERCENT }, 'ISPMT': { method: 'ispmt', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }] }, 'NOMINAL': { method: 'nominal', parameters: [{ argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, minValue: 1 }], returnNumberType: NumberType.NUMBER_PERCENT }, 'NPER': { method: 'nper', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }] }, 'PV': { method: 'pv', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'RATE': { method: 'rate', parameters: [{ argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, defaultValue: 0.1 }], returnNumberType: NumberType.NUMBER_PERCENT }, 'RRI': { method: 'rri', parameters: [{ argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }], returnNumberType: NumberType.NUMBER_PERCENT }, 'SLN': { method: 'sln', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'SYD': { method: 'syd', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'TBILLEQ': { method: 'tbilleq', parameters: [{ argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }], returnNumberType: NumberType.NUMBER_PERCENT }, 'TBILLPRICE': { method: 'tbillprice', parameters: [{ argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'TBILLYIELD': { method: 'tbillyield', parameters: [{ argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, minValue: 0 }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }], returnNumberType: NumberType.NUMBER_PERCENT }, 'FVSCHEDULE': { method: 'fvschedule', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.RANGE }], returnNumberType: NumberType.NUMBER_CURRENCY }, 'NPV': { method: 'npv', parameters: [{ argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.ANY }], repeatLastArgs: 1, returnNumberType: NumberType.NUMBER_CURRENCY }, 'MIRR': { method: 'mirr', parameters: [{ argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.NUMBER }], returnNumberType: NumberType.NUMBER_PERCENT }, 'PDURATION': { method: 'pduration', parameters: [{ argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }, { argumentType: FunctionArgumentType.NUMBER, greaterThan: 0 }] }, 'XNPV': { method: 'xnpv', parameters: [{ argumentType: FunctionArgumentType.NUMBER, greaterThan: -1 }, { argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.RANGE }] } }; function pmtCore(rate, periods, present, future, type) { if (rate === 0) { return (-present - future) / periods; } else { const term = Math.pow(1 + rate, periods); return (future * rate + present * rate * term) * (type ? 1 / (1 + rate) : 1) / (1 - term); } } function ipmtCore(rate, period, periods, present, future, type) { const payment = pmtCore(rate, periods, present, future, type); if (period === 1) { return rate * (type ? 0 : -present); } else { return rate * (type ? fvCore(rate, period - 2, payment, present, type) - payment : fvCore(rate, period - 1, payment, present, type)); } } function fvCore(rate, periods, payment, value, type) { if (rate === 0) { return -value - payment * periods; } else { const term = Math.pow(1 + rate, periods); return payment * (type ? 1 + rate : 1) * (1 - term) / rate - value * term; } } function ppmtCore(rate, period, periods, present, future, type) { return pmtCore(rate, periods, present, future, type) - ipmtCore(rate, period, periods, present, future, type); } function npvCore(rate, args) { let acc = 0; for (let i = args.length - 1; i >= 0; i--) { acc += args[i]; if (rate === -1) { if (acc === 0) { continue; } else { return new CellError(ErrorType.DIV_BY_ZERO); } } acc /= 1 + rate; } return acc; }