From dc341473254d4aa9838ceb78bf188c9aa2c119fb Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Sat, 21 Dec 2019 13:54:57 +0300 Subject: [PATCH] Financial functions: part 2 (#360) * DB, DDB * DISC, DOLLARDE, DOLLARFR * EFFECT * FV * FVSCHEDULE, INTRATE, IPMT * IRR, ISPMT * MIRR, NOMINAL, NPER, NPV --- spreadsheet/cell.go | 2 +- spreadsheet/formula/fnfinance.go | 798 +++++++++++++++++++++++++- spreadsheet/formula/functions_test.go | 324 +++++++++++ 3 files changed, 1119 insertions(+), 5 deletions(-) diff --git a/spreadsheet/cell.go b/spreadsheet/cell.go index 89136150d9..ce7dbc6928 100644 --- a/spreadsheet/cell.go +++ b/spreadsheet/cell.go @@ -169,7 +169,7 @@ func (c Cell) SetNumber(v float64) { // cell type number c.x.TAttr = sml.ST_CellTypeN - c.x.V = unioffice.String(strconv.FormatFloat(v, 'g', -1, 64)) + c.x.V = unioffice.String(strconv.FormatFloat(v, 'f', -1, 64)) } // Column returns the cell column diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index 492a948ff6..4aa8d6716f 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -10,13 +10,11 @@ package formula import ( "time" "math" + "strconv" + "strings" ) func init() { - RegisterFunction("DURATION", Duration) - RegisterFunction("MDURATION", Mduration) - RegisterFunction("PDURATION", Pduration) - RegisterFunction("_xlfn.PDURATION", Pduration) RegisterFunction("ACCRINTM", Accrintm) RegisterFunction("AMORDEGRC", Amordegrc) RegisterFunction("AMORLINC", Amorlinc) @@ -28,6 +26,26 @@ func init() { RegisterFunction("COUPPCD", Couppcd) RegisterFunction("CUMIPMT", Cumipmt) RegisterFunction("CUMPRINC", Cumprinc) + RegisterFunction("DB", Db) + RegisterFunction("DDB", Ddb) + RegisterFunction("DISC", Disc) + RegisterFunction("DOLLARDE", Dollarde) + RegisterFunction("DOLLARFR", Dollarfr) + RegisterFunction("DURATION", Duration) + RegisterFunction("EFFECT", Effect) + RegisterFunction("FV", Fv) + RegisterFunction("FVSCHEDULE", Fvschedule) + RegisterFunction("INTRATE", Intrate) + RegisterFunction("IPMT", Ipmt) + RegisterFunction("IRR", Irr) + RegisterFunction("ISPMT", Ispmt) + RegisterFunction("MDURATION", Mduration) + RegisterFunction("MIRR", Mirr) + RegisterFunction("NOMINAL", Nominal) + RegisterFunction("NPER", Nper) + RegisterFunction("NPV", Npv) + RegisterFunction("PDURATION", Pduration) + RegisterFunction("_xlfn.PDURATION", Pduration) } // Duration implements the Excel DURATION function. @@ -789,3 +807,775 @@ func fv(rate, periods, payment, value float64, t int) float64 { } return -result } + +// Db implements the Excel DB function. +func Db(args []Result) Result { + argsNum := len(args) + if argsNum != 4 && argsNum != 5 { + return MakeErrorResult("DB requires four or five number arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("DB requires cost to be number argument") + } + cost := args[0].ValueNumber + if cost < 0 { + return MakeErrorResultType(ErrorTypeNum, "DB requires cost to be non negative") + } + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("DB requires salvage to be number argument") + } + salvage := args[1].ValueNumber + if salvage < 0 { + return MakeErrorResultType(ErrorTypeNum, "DB requires salvage to be non negative") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("DB requires life to be number argument") + } + life := args[2].ValueNumber + if life <= 0 { + return MakeErrorResultType(ErrorTypeNum, "DB requires life to be positive") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("DB requires period to be number argument") + } + period := args[3].ValueNumber + if period <= 0 { + return MakeErrorResultType(ErrorTypeNum, "DB requires period to be positive") + } + if period - life > 1 { + return MakeErrorResultType(ErrorTypeNum, "Incorrect period for DB") + } + month := 12.0 + if argsNum == 5 { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("DB requires month to be number argument") + } + month = args[4].ValueNumber + if month < 1 || month > 12 { + return MakeErrorResultType(ErrorTypeNum, "DB requires month to be in range of 1 and 12") + } + } + if month == 12 && period > life { + return MakeErrorResultType(ErrorTypeNum, "Incorrect period for DB") + } + if salvage >= cost { + return MakeNumberResult(0) + } + rate := 1 - math.Pow(salvage / cost, 1 / life) + rate = float64(int(rate * 1000 + 0.5)) / 1000 // round to 3 decimal places + initial := cost * rate * month / 12 + if period == 1 { + return MakeNumberResult(initial) + } + total := initial + current := 0.0 + ceiling := life + if ceiling > period { + ceiling = period + } + for i := 2.0; i <= ceiling; i++ { + current = (cost - total) * rate + total += current + } + if period > life { + return MakeNumberResult((cost - total) * rate * (12 - month) / 12) + } + return MakeNumberResult(current) +} + +// Ddb implements the Excel DDB function. +func Ddb(args []Result) Result { + argsNum := len(args) + if argsNum != 4 && argsNum != 5 { + return MakeErrorResult("DDB requires four or five number arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("DDB requires cost to be number argument") + } + cost := args[0].ValueNumber + if cost < 0 { + return MakeErrorResultType(ErrorTypeNum, "DDB requires cost to be non negative") + } + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("DDB requires salvage to be number argument") + } + salvage := args[1].ValueNumber + if salvage < 0 { + return MakeErrorResultType(ErrorTypeNum, "DDB requires salvage to be non negative") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("DDB requires life to be number argument") + } + life := args[2].ValueNumber + if life <= 0 { + return MakeErrorResultType(ErrorTypeNum, "DDB requires life to be positive") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("DDB requires period to be number argument") + } + period := args[3].ValueNumber + if period < 1 { + return MakeErrorResultType(ErrorTypeNum, "DDB requires period to be positive") + } + if period > life { + return MakeErrorResultType(ErrorTypeNum, "Incorrect period for DDB") + } + factor := 2.0 + if argsNum == 5 { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("DDB requires factor to be number argument") + } + factor = args[4].ValueNumber + if factor < 0 { + return MakeErrorResultType(ErrorTypeNum, "DDB requires factor to be non negative") + } + } + + oldValue := 0.0 + rate := factor / life + if rate >= 1 { + rate = 1 + if period == 1 { + oldValue = cost + } + } else { + oldValue = cost * math.Pow(1 - rate, period - 1) + } + newValue := cost * math.Pow(1 - rate, period) + + var ddb float64 + + if newValue < salvage { + ddb = oldValue - salvage + } else { + ddb = oldValue - newValue + } + if ddb < 0 { + ddb = 0 + } + return MakeNumberResult(ddb) +} + +// Disc implements the Excel DISC function. +func Disc(args []Result) Result { + argsNum := len(args) + if argsNum != 4 && argsNum != 5 { + return MakeErrorResult("DISC requires four or five arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("DISC requires settlement date to be number argument") + } + settlement := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("DISC requires maturity date to be number argument") + } + maturity := args[1].ValueNumber + if settlement >= maturity { + return MakeErrorResultType(ErrorTypeNum, "DISC requires maturity date to be later than settlement date") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("DISC requires pr to be number argument") + } + pr := args[2].ValueNumber + if pr <= 0 { + return MakeErrorResultType(ErrorTypeNum, "DISC requires pr to be positive number argument") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("DISC requires redemption to be number argument") + } + redemption := args[3].ValueNumber + if redemption <= 0 { + return MakeErrorResultType(ErrorTypeNum, "DISC requires redemption to be positive number argument") + } + basis := 0 + if argsNum == 5 { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("DISC requires basis to be number argument") + } + basis = int(args[4].ValueNumber) + if !checkBasis(basis) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for DISC") + } + } + fracResult := yearFrac(settlement, maturity, basis) + if fracResult.Type == ResultTypeError { + return fracResult + } + return MakeNumberResult((redemption - pr) / redemption / fracResult.ValueNumber) +} + +// Dollarde implements the Excel DOLLARDE function. +func Dollarde(args []Result) Result { + dollar, fraction, resultErr := parseDollarArgs(args, "DOLLARDE") + if resultErr.Type == ResultTypeError { + return resultErr + } + if fraction < 1 { + return MakeErrorResultType(ErrorTypeDivideByZero, "DOLLARDE requires fraction to be equal or more than 1") + } + if dollar == 0 { + return MakeNumberResult(0) + } + neg := dollar < 0 + if neg { + dollar = -dollar + } + dollarStr := args[0].Value() + split := strings.Split(dollarStr, ".") + dollarInt := float64(int(dollar)) + dollarFracStr := split[1] + dollarFracOrder := len(dollarFracStr) + fractionOrder := int(math.Log10(fraction)) + 1 + power := float64(fractionOrder - dollarFracOrder) + dollarFrac, err := strconv.ParseFloat(dollarFracStr, 64) + if err != nil { + return MakeErrorResult("Incorrect fraction argument for DOLLARDE") + } + dollarFrac *= math.Pow(10, power) + dollarde := dollarInt + dollarFrac / fraction + if neg { + dollarde = -dollarde + } + return MakeNumberResult(dollarde) +} + +// Dollarfr implements the Excel DOLLARFR function. +func Dollarfr(args []Result) Result { + dollar, fraction, resultErr := parseDollarArgs(args, "DOLLARFR") + if resultErr.Type == ResultTypeError { + return resultErr + } + if dollar == 0 { + return MakeNumberResult(0) + } + neg := dollar < 0 + if neg { + dollar = -dollar + } + dollarInt := float64(int(dollar)) + dollarStr := args[0].Value() + split := strings.Split(dollarStr, ".") + dollarFracStr := split[1] + dollarFrac, err := strconv.ParseFloat(dollarFracStr, 64) + if err != nil { + return MakeErrorResult("Incorrect fraction argument for DOLLARFR") + } + dollarFracOrder := float64(len(dollarFracStr)) + dollarFrac /= math.Pow(10, dollarFracOrder) + + dollarfr := dollarFrac * fraction / math.Pow(10, float64(int(math.Log10(fraction))) + 1) + dollarInt + if neg { + dollarfr = -dollarfr + } + return MakeNumberResult(dollarfr) +} + +func parseDollarArgs(args []Result, funcName string) (float64, float64, Result) { + if len(args) != 2 { + return 0, 0, MakeErrorResult(funcName + " requires two arguments") + } + if args[0].Type != ResultTypeNumber { + return 0, 0, MakeErrorResult(funcName + " requires fractional dollar to be number argument") + } + dollar := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return 0, 0, MakeErrorResult(funcName + " requires fraction to be number argument") + } + fraction := float64(int(args[1].ValueNumber)) + if fraction < 0 { + return 0, 0, MakeErrorResultType(ErrorTypeNum, funcName + " requires fraction to be positive number") + } + return dollar, fraction, MakeEmptyResult() +} + +// Effect implements the Excel EFFECT function. +func Effect(args []Result) Result { + if len(args) != 2 { + return MakeErrorResult("EFFECT requires two arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("EFFECT requires nominal interest rate to be number argument") + } + nominal := args[0].ValueNumber + if nominal <= 0 { + return MakeErrorResultType(ErrorTypeNum, "EFFECT requires nominal interest rate to be positive number argument") + } + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("EFFECT requires number of compounding periods to be number argument") + } + npery := float64(int(args[1].ValueNumber)) + if npery < 1 { + return MakeErrorResultType(ErrorTypeNum, "EFFECT requires number of compounding periods to be 1 or more") + } + return MakeNumberResult(math.Pow((1 + nominal / npery), npery) - 1) +} + +// Fv implements the Excel FV function. +func Fv(args []Result) Result { + argsNum := len(args) + if argsNum < 3 || argsNum > 5 { + return MakeErrorResult("FV requires number of arguments in range of 3 and 5") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("FV requires rate to be number argument") + } + rate := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("FV requires number of periods to be number argument") + } + nPer := args[1].ValueNumber + if nPer != float64(int(nPer)) { + return MakeErrorResultType(ErrorTypeNum, "FV requires number of periods to be integer number argument") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("FV requires payment to be number argument") + } + pmt := args[2].ValueNumber + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("FV requires payment to be number argument") + } + pv := 0.0 + if argsNum >= 4 { + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("FV requires present value to be number argument") + } + pv = args[3].ValueNumber + } + t := 0 + if argsNum == 5 { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("FV requires type to be number argument") + } + t = int(args[4].ValueNumber) + if t != 0 { + t = 1 + } + } + return MakeNumberResult(fv(rate, nPer, pmt, pv, t)) +} + +// Fvschedule implements the Excel FVSCHEDULE function. +func Fvschedule(args []Result) Result { + if len(args) != 2 { + return MakeErrorResult("FVSCHEDULE requires two arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("FVSCHEDULE requires principal to be number argument") + } + principal := args[0].ValueNumber + switch args[1].Type { + case ResultTypeNumber: + return MakeNumberResult(principal * (args[1].ValueNumber+1)) + case ResultTypeList, ResultTypeArray: + schedule := arrayFromRange(args[1]) + for _, row := range schedule { + for _, rate := range row { + if rate.Type != ResultTypeNumber || rate.IsBoolean { + return MakeErrorResult("FVSCHEDULE requires rates to be numbers") + } + principal *= 1.0 + rate.ValueNumber + } + } + return MakeNumberResult(principal) + default: + return MakeErrorResult("FVSCHEDULE requires schedule to be of array type") + } +} + +// Intrate implements the Excel INTRATE function. +func Intrate(args []Result) Result { + argsNum := len(args) + if argsNum != 4 && argsNum != 5 { + return MakeErrorResult("INTRATE requires four or five arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("INTRATE requires settlement date to be number argument") + } + settlement := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("INTRATE requires maturity date to be number argument") + } + maturity := args[1].ValueNumber + if settlement >= maturity { + return MakeErrorResultType(ErrorTypeNum, "INTRATE requires maturity date to be later than settlement date") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("INTRATE requires investment to be number argument") + } + investment := args[2].ValueNumber + if investment <= 0 { + return MakeErrorResultType(ErrorTypeNum, "INTRATE requires investment to be positive number argument") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("INTRATE requires redemption to be number argument") + } + redemption := args[3].ValueNumber + if redemption <= 0 { + return MakeErrorResultType(ErrorTypeNum, "INTRATE requires redemption to be positive number argument") + } + basis := 0 + if argsNum == 5 { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("INTRATE requires basis to be number argument") + } + basis = int(args[4].ValueNumber) + if !checkBasis(basis) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for INTRATE") + } + } + fracResult := yearFrac(settlement, maturity, basis) + if fracResult.Type == ResultTypeError { + return fracResult + } + return MakeNumberResult((redemption - investment) / investment / fracResult.ValueNumber) +} + +// Ipmt implements the Excel IPMT function. +func Ipmt(args []Result) Result { + argsNum := len(args) + if argsNum < 4 || argsNum > 6 { + return MakeErrorResult("IPMT requires six arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("IPMT requires rate to be number argument") + } + rate := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("IPMT requires period to be number argument") + } + period := args[1].ValueNumber + if period <= 0 { + return MakeErrorResultType(ErrorTypeNum, "IPMT requires period to be positive number argument") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("IPMT requires number of periods to be number argument") + } + nPer := args[2].ValueNumber + if nPer <= 0 { + return MakeErrorResultType(ErrorTypeNum, "IPMT requires number of periods to be positive number argument") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("IPMT requires present value to be number argument") + } + presentValue := args[3].ValueNumber + futureValue := 0.0 + if argsNum > 4 { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("IPMT requires future value to be number argument") + } + futureValue = args[4].ValueNumber + } + t := 0 + if argsNum == 6 { + if args[5].Type != ResultTypeNumber { + return MakeErrorResult("IPMT requires start period to be number argument") + } + t = int(args[5].ValueNumber) + if t != 0 { + t = 1 + } + } + payment := pmt(rate, nPer, presentValue, futureValue, t) + var interest float64 + if period == 1 { + if t == 1 { + interest = 0 + } else { + interest = -presentValue + } + } else { + if t == 1 { + interest = fv(rate, period - 2, payment, presentValue, 1) - payment + } else { + interest = fv(rate, period - 1, payment, presentValue, 0) + } + } + + return MakeNumberResult(interest * rate) +} + +// Irr implements the Excel IRR function. +func Irr(args []Result) Result { + argsNum := len(args) + if argsNum > 2 { + return MakeErrorResult("IRR requires one or two arguments") + } + if args[0].Type != ResultTypeList && args[0].Type != ResultTypeArray { + return MakeErrorResult("IRR requires values to be range argument") + } + valuesR := arrayFromRange(args[0]) + values := []float64{} + for _, row := range valuesR { + for _, vR := range row { + if vR.Type == ResultTypeNumber && !vR.IsBoolean { + values = append(values, vR.ValueNumber) + } + } + } + vlen := len(values) + if len(values) < 2 { + return MakeErrorResultType(ErrorTypeNum, "") + } + guess := 0.1 + if argsNum == 2 { + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("IRR requires guess to be number argument") + } + guess = args[1].ValueNumber + if guess <= -1 { + return MakeErrorResult("IRR requires guess to be more than -1") + } + } + + dates := []float64{} + positive := false + negative := false + + for i := 0; i < vlen; i++ { + if i == 0 { + dates = append(dates, 0) + } else { + dates = append(dates, dates[i - 1] + 365) + } + if values[i] > 0 { + positive = true + } + if values[i] < 0 { + negative = true + } + } + + if !positive || !negative { + return MakeErrorResultType(ErrorTypeNum, "") + } + + resultRate := guess + epsMax := 1e-10 + iter := 0 + maxIter := 50 + isErr := false + + for { + resultValue := irrResult(values, dates, resultRate) + newRate := resultRate - resultValue / irrResultDeriv(values, dates, resultRate) + epsRate := math.Abs(newRate - resultRate) + resultRate = newRate + iter++ + if iter > maxIter || epsRate <= epsMax || math.Abs(resultValue) <= epsMax { + break + } + if iter > maxIter { + isErr = true + break + } + } + if isErr || math.IsNaN(resultRate) || math.IsInf(resultRate, 0) { + return MakeErrorResultType(ErrorTypeNum, "") + } + return MakeNumberResult(resultRate) +} + +func irrResult(values, dates []float64, rate float64) float64 { + r := rate + 1 + result := values[0] + vlen := len(values) + firstDate := dates[0] + for i := 1; i < vlen; i++ { + result += values[i] / math.Pow(r, (dates[i] - firstDate) / 365) + } + return result +} + +func irrResultDeriv(values, dates []float64, rate float64) float64 { + r := rate + 1 + result := 0.0 + vlen := len(values) + firstDate := dates[0] + for i := 1; i < vlen; i++ { + frac := (dates[i] - firstDate) / 365 + result -= frac * values[i] / math.Pow(r, frac + 1) + } + return result +} + +// Ispmt implements the Excel ISPMT function. +func Ispmt(args []Result) Result { + if len(args) != 4 { + return MakeErrorResult("ISPMT requires six arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("ISPMT requires rate to be number argument") + } + rate := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("ISPMT requires period to be number argument") + } + period := args[1].ValueNumber + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("ISPMT requires number of periods to be number argument") + } + nPer := args[2].ValueNumber + if nPer <= 0 { + return MakeErrorResultType(ErrorTypeNum, "ISPMT requires number of periods to be positive number argument") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("ISPMT requires present value to be number argument") + } + pv := args[3].ValueNumber + + return MakeNumberResult(pv * rate * (period / nPer - 1)) +} + +// Mirr implements the Excel MIRR function. +func Mirr(args []Result) Result { + if len(args) != 3 { + return MakeErrorResult("MIRR requires three arguments") + } + if args[0].Type != ResultTypeList && args[0].Type != ResultTypeArray { + return MakeErrorResult("MIRR requires values to be range argument") + } + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("MIRR requires finance rate to be number argument") + } + finRate := args[1].ValueNumber + 1 + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("MIRR requires reinvest rate to be number argument") + } + reinvRate := args[2].ValueNumber + 1 + if reinvRate == 0 { + return MakeErrorResultType(ErrorTypeDivideByZero, "") + } + + valuesR := arrayFromRange(args[0]) + n := float64(len(valuesR)) + npvInvest, npvReinvest := 0.0, 0.0 + powInvest, powReinvest := 1.0, 1.0 + hasPositive, hasNegative := false, false + for _, row := range valuesR { + for _, vR := range row { + if vR.Type == ResultTypeNumber && !vR.IsBoolean { + v := vR.ValueNumber + if v == 0 { + continue + } else { + if v > 0 { + hasPositive = true + npvReinvest += vR.ValueNumber * powReinvest + } else { + hasNegative = true + npvInvest += vR.ValueNumber * powInvest + } + powInvest /= finRate + powReinvest /= reinvRate + } + } + } + } + + if !hasPositive || !hasNegative { + return MakeErrorResultType(ErrorTypeDivideByZero, "") + } + + result := -npvReinvest / npvInvest + result *= math.Pow(reinvRate, n - 1) + result = math.Pow(result, 1 / (n - 1)) + return MakeNumberResult(result - 1) +} + +// Nominal implements the Excel NOMINAL function. +func Nominal(args []Result) Result { + if len(args) != 2 { + return MakeErrorResult("NOMINAL requires two arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("NOMINAL requires nominal interest rate to be number argument") + } + effect := args[0].ValueNumber + if effect <= 0 { + return MakeErrorResultType(ErrorTypeNum, "NOMINAL requires effect interest rate to be positive number argument") + } + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("NOMINAL requires number of compounding periods to be number argument") + } + npery := float64(int(args[1].ValueNumber)) + if npery < 1 { + return MakeErrorResultType(ErrorTypeNum, "NOMINAL requires number of compounding periods to be 1 or more") + } + return MakeNumberResult((math.Pow(effect + 1, 1 / npery) - 1) * npery) +} + +// Nper implements the Excel NPER function. +func Nper(args []Result) Result { + argsNum := len(args) + if argsNum < 3 || argsNum > 5 { + return MakeErrorResult("NPER requires number of arguments in range of 3 and 5") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("NPER requires rate to be number argument") + } + rate := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("NPER requires payment to be number argument") + } + pmt := args[1].ValueNumber + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("NPER requires present value to be number argument") + } + pv := args[2].ValueNumber + fv := 0.0 + if argsNum >= 4 { + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("NPER requires future value to be number argument") + } + fv = args[3].ValueNumber + } + t := 0.0 + if argsNum == 5 { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("NPER requires type to be number argument") + } + t = args[4].ValueNumber + if t != 0 { + t = 1 + } + } + num := pmt * (1 + rate * t) - fv * rate + den := (pv * rate + pmt * (1 + rate * t)) + return MakeNumberResult(math.Log(num / den) / math.Log(1 + rate)) +} + +// Npv implements the Excel NPV function. +func Npv(args []Result) Result { + argsNum := len(args) + if argsNum < 2 { + return MakeErrorResult("NPV requires two or more arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("NPV requires rate to be number argument") + } + rate := args[0].ValueNumber + if rate == -1 { + return MakeErrorResultType(ErrorTypeDivideByZero, "") + } + values := []float64{} + for _, arg := range args[1:] { + switch arg.Type { + case ResultTypeNumber: + values = append(values, arg.ValueNumber) + case ResultTypeArray, ResultTypeList: + rangeR := arrayFromRange(arg) + for _, r := range rangeR { + for _, vR := range r { + if vR.Type == ResultTypeNumber && !vR.IsBoolean { + values = append(values, vR.ValueNumber) + } + } + } + } + } + npv := 0.0 + for i, value := range values { + npv += value / math.Pow(1 + rate, float64(i) + 1) + } + return MakeNumberResult(npv) +} diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index 15ed98206c..05fa889d59 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -1868,3 +1868,327 @@ func TestCumprinc(t *testing.T) { runTests(t, ctx, td) } + +func TestDb(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(1000000) + sheet.Cell("A2").SetNumber(100000) + sheet.Cell("A3").SetNumber(6) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=DB(A1,A2,A3,1,7)`, `186083.333333 ResultTypeNumber`}, + {`=DB(A1,A2,A3,2,7)`, `259639.416666 ResultTypeNumber`}, + {`=DB(A1,A2,A3,3,7)`, `176814.44275 ResultTypeNumber`}, + {`=DB(A1,A2,A3,4,7)`, `120410.635512 ResultTypeNumber`}, + {`=DB(A1,A2,A3,5,7)`, `81999.6427841 ResultTypeNumber`}, + {`=DB(A1,A2,A3,6,7)`, `55841.756736 ResultTypeNumber`}, + {`=DB(A1,A2,A3,7,7)`, `15845.0984738 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestDdb(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(2400) + sheet.Cell("A2").SetNumber(300) + sheet.Cell("A3").SetNumber(10) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=DDB(A1,A2,A3*365,1)`, `1.31506849315 ResultTypeNumber`}, + {`=DDB(A1,A2,A3*12,1,2)`, `40 ResultTypeNumber`}, + {`=DDB(A1,A2,A3,1,2)`, `480 ResultTypeNumber`}, + {`=DDB(A1,A2,A3,2,1.5)`, `306. ResultTypeNumber`}, + {`=DDB(A1,A2,A3,10)`, `22.1225472 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestDisc(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetDate(time.Date(2018, 7, 1, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A2").SetDate(time.Date(2048, 1, 1, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A3").SetNumber(97.975) + sheet.Cell("A4").SetNumber(100) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=DISC(A1,A2,A3,A4,0)`, `0.00068644067 ResultTypeNumber`}, + {`=DISC(A1,A2,A3,A4,1)`, `0.00068638416 ResultTypeNumber`}, + {`=DISC(A1,A2,A3,A4,2)`, `0.00067650334 ResultTypeNumber`}, + {`=DISC(A1,A2,A3,A4,3)`, `0.00068589922 ResultTypeNumber`}, + {`=DISC(A1,A2,A3,A4,4)`, `0.00068644067 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestDollarde(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=DOLLARDE(1.02,16)`, `1.125 ResultTypeNumber`}, + {`=DOLLARDE(1.1,32)`, `1.3125 ResultTypeNumber`}, + {`=DOLLARDE(-1.1,32)`, `-1.3125 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestDollarfr(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=DOLLARFR(1.125,16)`, `1.02 ResultTypeNumber`}, + {`=DOLLARFR(1.125,32)`, `1.04 ResultTypeNumber`}, + {`=DOLLARFR(-1.125,32)`, `-1.04 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestEffect(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=EFFECT(0.0525,4)`, `0.05354266737 ResultTypeNumber`}, + {`=EFFECT(0.1,4)`, `0.10381289062 ResultTypeNumber`}, + {`=EFFECT(0.1,4.5)`, `0.10381289062 ResultTypeNumber`}, + {`=EFFECT(0,4.5)`, `#NUM! ResultTypeError`}, + {`=EFFECT(0.1,0.5)`, `#NUM! ResultTypeError`}, + {`=EFFECT("Hello world",4)`, `#VALUE! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestFv(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=FV(0.06/12,10,-200,-500,1)`, `2581.40337406 ResultTypeNumber`}, + {`=FV(0,12,-100,-1000,1)`, `2200 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestFvschedule(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(0.09) + sheet.Cell("A2").SetNumber(0.11) + sheet.Cell("A3").SetNumber(0.1) + sheet.Cell("A4").SetBool(true) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=FVSCHEDULE(1,A1:A3)`, `1.33089 ResultTypeNumber`}, + {`=FVSCHEDULE(1,A1:A4)`, `#VALUE! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestIntrate(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetDate(time.Date(2008, 2, 15, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A2").SetDate(time.Date(2008, 5, 15, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A3").SetNumber(1000000) + sheet.Cell("A4").SetNumber(1014420) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=INTRATE(A1,A2,A3,A4,0)`, `0.05768 ResultTypeNumber`}, + {`=INTRATE(A1,A2,A3,A4,1)`, `0.05864133333 ResultTypeNumber`}, + {`=INTRATE(A1,A2,A3,A4,2)`, `0.05768 ResultTypeNumber`}, + {`=INTRATE(A1,A2,A3,A4,3)`, `0.05848111111 ResultTypeNumber`}, + {`=INTRATE(A1,A2,A3,A4,4)`, `0.05768 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestIpmt(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(0.1) + sheet.Cell("A2").SetNumber(1) + sheet.Cell("A3").SetNumber(3) + sheet.Cell("A4").SetNumber(8000) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=IPMT(0.1/12,1,36,8000)`, `-66.666666666 ResultTypeNumber`}, + {`=IPMT(0.1,3,3,8000)`, `-292.4471299 ResultTypeNumber`}, + {`=IPMT(0.1/12,6,24,100000,1000000,0)`, `928.82357184 ResultTypeNumber`}, + {`=IPMT(0.1/12,6,24,100000,1000000,1)`, `921.147343973 ResultTypeNumber`}, + {`=IPMT(0.1/12,1,24,100000,1000000,1)`, `0 ResultTypeNumber`}, + {`=IPMT(0.1/12,1,24,100000,1000000,0)`, `-833.33333333 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestIrr(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(-70000) + sheet.Cell("A2").SetNumber(12000) + sheet.Cell("A3").SetNumber(15000) + sheet.Cell("A4").SetNumber(18000) + sheet.Cell("A5").SetNumber(21000) + sheet.Cell("A6").SetNumber(26000) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=IRR(A1:A6)`, `0.08663094803 ResultTypeNumber`}, + {`=IRR(A1:A5)`, `-0.0212448482 ResultTypeNumber`}, + {`=IRR(A1:A4)`, `-0.1821374641 ResultTypeNumber`}, + {`=IRR(A1:A3,0.2)`, `-0.4435069413 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestIspmt(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(0.1) + sheet.Cell("A2").SetNumber(4) + sheet.Cell("A3").SetNumber(4000) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=ISPMT(0.1/12,6,2*12,100000)`, `-625 ResultTypeNumber`}, + {`=ISPMT(A1,0,A2,A3)`, `-400 ResultTypeNumber`}, + {`=ISPMT(A1,1,A2,A3)`, `-300 ResultTypeNumber`}, + {`=ISPMT(A1,2,A2,A3)`, `-200 ResultTypeNumber`}, + {`=ISPMT(A1,3,A2,A3)`, `-100 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestMirr(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(-120000) + sheet.Cell("A2").SetNumber(39000) + sheet.Cell("A3").SetNumber(30000) + sheet.Cell("A4").SetNumber(21000) + sheet.Cell("A5").SetNumber(37000) + sheet.Cell("A6").SetNumber(46000) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=MIRR(A1:A6,0.1,0.12)`, `0.12609413036 ResultTypeNumber`}, + {`=MIRR(A1:A4,0.1,0.12)`, `-0.0480446552 ResultTypeNumber`}, + {`=MIRR(A1:A6,0.1,0.14)`, `0.13475911082 ResultTypeNumber`}, + {`=MIRR(A1:A6,0.2,0.14)`, `0.13475911082 ResultTypeNumber`}, + {`=MIRR(A1:A6,0.3,0.14)`, `0.13475911082 ResultTypeNumber`}, + {`=MIRR(A1:A6,0.4,0.14)`, `0.13475911082 ResultTypeNumber`}, + {`=MIRR(A1:A6,0,0.14)`, `0.13475911082 ResultTypeNumber`}, + {`=MIRR(A1:A6,-1,0.14)`, `0.13475911082 ResultTypeNumber`}, + {`=MIRR(A1:A6,0.1,-1)`, `#DIV/0! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestNominal(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=NOMINAL(0.053543,4)`, `0.05250031986 ResultTypeNumber`}, + {`=NOMINAL(0.1,4)`, `0.09645475633 ResultTypeNumber`}, + {`=NOMINAL(0.1,4.5)`, `0.09645475633 ResultTypeNumber`}, + {`=NOMINAL(0,4.5)`, `#NUM! ResultTypeError`}, + {`=NOMINAL(0.1,0.5)`, `#NUM! ResultTypeError`}, + {`=NOMINAL("Hello world",4.5)`, `#VALUE! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestNper(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(0.12) + sheet.Cell("A2").SetNumber(-100) + sheet.Cell("A3").SetNumber(-1000) + sheet.Cell("A4").SetNumber(10000) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=NPER(A1/12,A2,A3,A4,1)`, `59.6738656742 ResultTypeNumber`}, + {`=NPER(A1/12,A2,A3,A4)`, `60.0821228537 ResultTypeNumber`}, + {`=NPER(A1/12,A2,A3)`, `-9.5785940398 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestNpv(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(0.1) + sheet.Cell("A2").SetNumber(-10000) + sheet.Cell("A3").SetNumber(3000) + sheet.Cell("A4").SetNumber(4200) + sheet.Cell("A5").SetNumber(6800) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=NPV(A1,A2,A3,A4,A5)`, `1188.44341233 ResultTypeNumber`}, + {`=NPV(A1,A2:A4,A5)`, `1188.44341233 ResultTypeNumber`}, + {`=NPV(A1,A2:A4,"Hello world",A5)`, `1188.44341233 ResultTypeNumber`}, + {`=NPV(0.12,12000,15000,18000,21000,24000)`, `62448.3625219 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +}