From 88f5df85a29e7f41a6f188b15058c68410098795 Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Mon, 23 Dec 2019 14:06:05 +0300 Subject: [PATCH 01/12] PMT, PPMT --- spreadsheet/formula/fndatetime.go | 24 ++- spreadsheet/formula/fnfinance.go | 262 ++++++++++++++++++-------- spreadsheet/formula/functions_test.go | 39 ++++ 3 files changed, 239 insertions(+), 86 deletions(-) diff --git a/spreadsheet/formula/fndatetime.go b/spreadsheet/formula/fndatetime.go index 479ee1039f..bda4f9b1a3 100644 --- a/spreadsheet/formula/fndatetime.go +++ b/spreadsheet/formula/fndatetime.go @@ -783,14 +783,18 @@ func YearFrac(ctx Context, ev Evaluator, args []Result) Result { if err != nil { return MakeErrorResult("incorrect end date") } - return yearFracFromTime(startDate, endDate, basis) + yf, errResult := yearFracFromTime(startDate, endDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + return MakeNumberResult(yf) } -func yearFrac(startDate, endDate float64, basis int) Result { +func yearFrac(startDate, endDate float64, basis int) (float64, Result) { return yearFracFromTime(dateFromDays(startDate), dateFromDays(endDate), basis) } -func yearFracFromTime(startDate, endDate time.Time, basis int) Result { +func yearFracFromTime(startDate, endDate time.Time, basis int) (float64, Result) { startDateS := startDate.Unix() endDateS := endDate.Unix() sy, sm, sd := startDate.Date() @@ -806,27 +810,27 @@ func yearFracFromTime(startDate, endDate time.Time, basis int) Result { } else if sd == 30 && ed == 31 { ed = 30 } - return MakeNumberResult(float64(((ed + int(em) * 30 + ey * 360) - (sd + int(sm) * 30 + sy * 360))) / 360) + return float64(((ed + int(em) * 30 + ey * 360) - (sd + int(sm) * 30 + sy * 360))) / 360, MakeEmptyResult() case 1: var ylength = 365.0 if (sy == ey || ((sy + 1) == ey) && ((sm > em) || ((sm == em) && (sd >= ed)))) { if ((sy == ey && isLeapYear(sy)) || feb29Between(startDate, endDate) || (em == 1 && ed == 29)) { ylength = 366.0 } - return MakeNumberResult(daysBetween(startDateS, endDateS) / ylength) + return daysBetween(startDateS, endDateS) / ylength, MakeEmptyResult() } var years = float64((ey - sy) + 1) var days = float64((makeDateS(ey + 1, time.January, 1) - makeDateS(sy, time.January, 1)) / 86400) var average = days / years - return MakeNumberResult(daysBetween(startDateS, endDateS) / average) + return daysBetween(startDateS, endDateS) / average, MakeEmptyResult() case 2: - return MakeNumberResult(daysBetween(startDateS, endDateS) / 360.0) + return daysBetween(startDateS, endDateS) / 360.0, MakeEmptyResult() case 3: - return MakeNumberResult(daysBetween(startDateS, endDateS) / 365.0) + return daysBetween(startDateS, endDateS) / 365.0, MakeEmptyResult() case 4: - return MakeNumberResult(float64(((ed + int(em) * 30 + ey * 360) - (sd + int(sm) * 30 + sy * 360))) / 360.0) + return float64(((ed + int(em) * 30 + ey * 360) - (sd + int(sm) * 30 + sy * 360))) / 360.0, MakeEmptyResult() } - return MakeErrorResultType(ErrorTypeValue, "") + return 0, MakeErrorResult("") } func getDaysInYear(year, basis int) int { diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index 4aa8d6716f..89aaf04c3b 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -45,6 +45,8 @@ func init() { RegisterFunction("NPER", Nper) RegisterFunction("NPV", Npv) RegisterFunction("PDURATION", Pduration) + RegisterFunction("PMT", Pmt) + RegisterFunction("PPMT", Ppmt) RegisterFunction("_xlfn.PDURATION", Pduration) } @@ -127,12 +129,14 @@ func Coupdaybs(args []Result) Result { if err.Type == ResultTypeError { return err } - settlementDate := dateFromDays(parsedArgs.settlementDate) - maturityDate := dateFromDays(parsedArgs.maturityDate) - freq := parsedArgs.freq - basis := parsedArgs.basis + return MakeNumberResult(coupdaybs(parsedArgs.settlementDate, parsedArgs.maturityDate, parsedArgs.freq, parsedArgs.basis)) +} + +func coupdaybs(settlementDateF, maturityDateF float64, freq, basis int) float64 { + settlementDate := dateFromDays(settlementDateF) + maturityDate := dateFromDays(maturityDateF) pcd := couppcd(settlementDate, maturityDate, freq, basis) - return MakeNumberResult(getDiff(pcd, settlementDate, basis)) + return getDiff(pcd, settlementDate, basis) } // Coupdays implements the Excel COUPDAYS function. @@ -141,16 +145,18 @@ func Coupdays(args []Result) Result { if err.Type == ResultTypeError { return err } - settlementDate := dateFromDays(parsedArgs.settlementDate) - maturityDate := dateFromDays(parsedArgs.maturityDate) - freq := parsedArgs.freq - basis := parsedArgs.basis + return MakeNumberResult(coupdays(parsedArgs.settlementDate, parsedArgs.maturityDate, parsedArgs.freq, parsedArgs.basis)) +} + +func coupdays(settlementDateF, maturityDateF float64, freq, basis int) float64 { + settlementDate := dateFromDays(settlementDateF) + maturityDate := dateFromDays(maturityDateF) if basis == 1 { pcd := couppcd(settlementDate, maturityDate, freq, 1) next := pcd.AddDate(0, 12 / freq, 0) - return MakeNumberResult(getDiff(pcd, next, basis)) + return getDiff(pcd, next, basis) } - return MakeNumberResult(float64(getDaysInYear(0, basis)) / float64(freq)) + return float64(getDaysInYear(0, basis)) / float64(freq) } // Coupdaysnc implements the Excel COUPDAYSNC function. @@ -159,12 +165,14 @@ func Coupdaysnc(args []Result) Result { if err.Type == ResultTypeError { return err } - settlementDate := dateFromDays(parsedArgs.settlementDate) - maturityDate := dateFromDays(parsedArgs.maturityDate) - freq := parsedArgs.freq - basis := parsedArgs.basis + return MakeNumberResult(coupdaysnc(parsedArgs.settlementDate, parsedArgs.maturityDate, parsedArgs.freq, parsedArgs.basis)) +} + +func coupdaysnc(settlementDateF, maturityDateF float64, freq, basis int) float64 { + settlementDate := dateFromDays(settlementDateF) + maturityDate := dateFromDays(maturityDateF) ncd := coupncd(settlementDate, maturityDate, freq) - return MakeNumberResult(getDiff(settlementDate, ncd, basis)) + return getDiff(settlementDate, ncd, basis) } // Couppcd implements the Excel COUPPCD function. @@ -188,11 +196,9 @@ func Coupnum(args []Result) Result { if err.Type == ResultTypeError { return err } - settlementDate := dateFromDays(parsedArgs.settlementDate) - maturityDate := dateFromDays(parsedArgs.maturityDate) freq := parsedArgs.freq basis := parsedArgs.basis - cn, err := coupnum(settlementDate, maturityDate, freq, basis) + cn, err := coupnum(parsedArgs.settlementDate, parsedArgs.maturityDate, freq, basis) if err.Type == ResultTypeError { return err } @@ -284,7 +290,8 @@ func couppcd(settlementDate, maturityDate time.Time, freq, basis int) time.Time } // coupnum gets count of coupon dates. -func coupnum(settlementDate, maturityDate time.Time, freq, basis int) (float64, Result) { +func coupnum(settlementDateF, maturityDateF float64, freq, basis int) (float64, Result) { + settlementDate, maturityDate := dateFromDays(settlementDateF), dateFromDays(maturityDateF) if maturityDate.After(settlementDate) { aDate := couppcd(settlementDate, maturityDate, freq, basis) months := (maturityDate.Year() - aDate.Year()) * 12 + int(maturityDate.Month()) - int(aDate.Month()) @@ -295,12 +302,11 @@ func coupnum(settlementDate, maturityDate time.Time, freq, basis int) (float64, // getDuration returns the Macauley duration for an assumed par value of $100. It is defined as the weighted average of the present value of cash flows, and is used as a measure of a bond price's response to changes in yield. func getDuration(settlementDate, maturityDate, coup, yield, freq float64, basis int) Result { - fracResult := yearFrac(settlementDate, maturityDate, basis) - if fracResult.Type == ResultTypeError { - return fracResult + frac, errResult := yearFrac(settlementDate, maturityDate, basis) + if errResult.Type == ResultTypeError { + return errResult } - frac := fracResult.ValueNumber - coups, err := coupnum(dateFromDays(settlementDate), dateFromDays(maturityDate), int(freq), basis) + coups, err := coupnum(settlementDate, maturityDate, int(freq), basis) if err.Type == ResultTypeError { return err } @@ -350,7 +356,7 @@ func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { case ResultTypeString: settlementFromString := DateValue([]Result{settlementResult}) if settlementFromString.Type == ResultTypeError { - return nil, MakeErrorResult("Incorrect settltment date for " + funcName) + return nil, MakeErrorResult("Incorrect settlement date for " + funcName) } settlementDate = settlementFromString.ValueNumber default: @@ -363,7 +369,7 @@ func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { case ResultTypeString: maturityFromString := DateValue([]Result{maturityResult}) if maturityFromString.Type == ResultTypeError { - return nil, MakeErrorResult("Incorrect settltment date for " + funcName) + return nil, MakeErrorResult("Incorrect maturity date for " + funcName) } maturityDate = maturityFromString.ValueNumber default: @@ -404,7 +410,7 @@ func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { } basis = int(basisResult.ValueNumber) if !checkBasis(basis) { - return nil, MakeErrorResultType(ErrorTypeNum, "Incorrect basis value") + return nil, MakeErrorResultType(ErrorTypeNum, "Incorrect basis value for " + funcName) } } return &durationArgs{ @@ -466,11 +472,11 @@ func Accrintm(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for ACCRINTM") } } - fracResult := yearFrac(issue, settlement, basis) - if fracResult.Type == ResultTypeError { - return fracResult + frac, errResult := yearFrac(issue, settlement, basis) + if errResult.Type == ResultTypeError { + return errResult } - return MakeNumberResult(par * rate * fracResult.ValueNumber) + return MakeNumberResult(par * rate * frac) } // Amordegrc implements the Excel AMORDEGRC function. @@ -501,11 +507,11 @@ func Amordegrc(args []Result) Result { } rate *= amorCoeff - yfResult := yearFrac(datePurchased, firstPeriod, basis) - if yfResult.Type == ResultTypeError { + yf, errResult := yearFrac(datePurchased, firstPeriod, basis) + if errResult.Type == ResultTypeError { return MakeErrorResult("incorrect dates for AMORDEGRC") } - nRate := mathRound(yfResult.ValueNumber * rate * cost) + nRate := mathRound(yf * rate * cost) cost -= nRate rest := cost - salvage @@ -541,11 +547,11 @@ func Amorlinc(args []Result) Result { rate := parsedArgs.rate basis := parsedArgs.basis - yfResult := yearFrac(datePurchased, firstPeriod, basis) - if yfResult.Type == ResultTypeError { + yf, errResult := yearFrac(datePurchased, firstPeriod, basis) + if errResult.Type == ResultTypeError { return MakeErrorResult("incorrect dates for AMORLINC") } - r0 := yfResult.ValueNumber * rate * cost + r0 := yf * rate * cost if period == 0 { return MakeNumberResult(r0) } @@ -583,14 +589,14 @@ func parseAmorArgs(args []Result, funcName string) (*amorArgs, Result) { } cost := args[0].ValueNumber if cost < 0 { - return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires cost to be positive") + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires cost to be non negative") } if args[1].Type != ResultTypeNumber { return nil, MakeErrorResult(funcName + " requires date purchased to be number argument") } datePurchased := args[1].ValueNumber if datePurchased < 0 { - return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires date purchased to be positive") + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires date purchased to be non negative") } if args[2].Type != ResultTypeNumber { return nil, MakeErrorResult(funcName + " requires first period to be number argument") @@ -648,7 +654,7 @@ func mathRound(x float64) float64 { type cumulArgs struct { rate float64 nPer float64 - pv float64 + presentValue float64 startPeriod float64 endPeriod float64 t int @@ -662,24 +668,24 @@ func Cumipmt(args []Result) Result { } rate := parsedArgs.rate nPer := parsedArgs.nPer - pv := parsedArgs.pv + presentValue := parsedArgs.presentValue startPeriod := parsedArgs.startPeriod endPeriod := parsedArgs.endPeriod t := parsedArgs.t - payment := pmt(rate, nPer, pv, 0, t) + payment := pmt(rate, nPer, presentValue, 0, t) interest := 0.0 if startPeriod == 1 { if t == 0 { - interest = -pv + interest = -presentValue startPeriod++ } } for i := startPeriod; i <= endPeriod; i++ { if t == 1 { - interest += fv(rate, i - 2, payment, pv, 1) - payment + interest += fv(rate, i - 2, payment, presentValue, 1) - payment } else { - interest += fv(rate, i - 1, payment, pv, 0) + interest += fv(rate, i - 1, payment, presentValue, 0) } } interest *= rate @@ -694,16 +700,16 @@ func Cumprinc(args []Result) Result { } rate := parsedArgs.rate nPer := parsedArgs.nPer - pv := parsedArgs.pv + presentValue := parsedArgs.presentValue startPeriod := parsedArgs.startPeriod endPeriod := parsedArgs.endPeriod t := parsedArgs.t - payment := pmt(rate, nPer, pv, 0, t) + payment := pmt(rate, nPer, presentValue, 0, t) principal := 0.0 if startPeriod == 1 { if t == 0 { - principal = payment + pv * rate + principal = payment + presentValue * rate } else { principal = payment } @@ -711,9 +717,9 @@ func Cumprinc(args []Result) Result { } for i := startPeriod; i <= endPeriod; i++ { if t == 1 { - principal += payment - (fv(rate, i - 2, payment, pv, 1) - payment) * rate + principal += payment - (fv(rate, i - 2, payment, presentValue, 1) - payment) * rate } else { - principal += payment - fv(rate, i - 1, payment, pv, 0) * rate + principal += payment - fv(rate, i - 1, payment, presentValue, 0) * rate } } return MakeNumberResult(principal) @@ -740,8 +746,8 @@ func parseCumulArgs(args []Result, funcName string) (*cumulArgs, Result) { if args[2].Type != ResultTypeNumber { return nil, MakeErrorResult(funcName + " requires present value to be number argument") } - pv := args[2].ValueNumber - if pv <= 0 { + presentValue := args[2].ValueNumber + if presentValue <= 0 { return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires present value to be positive number argument") } if args[3].Type != ResultTypeNumber { @@ -771,7 +777,7 @@ func parseCumulArgs(args []Result, funcName string) (*cumulArgs, Result) { return &cumulArgs{ rate, nPer, - pv, + presentValue, startPeriod, endPeriod, t, @@ -997,11 +1003,11 @@ func Disc(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for DISC") } } - fracResult := yearFrac(settlement, maturity, basis) - if fracResult.Type == ResultTypeError { - return fracResult + frac, errResult := yearFrac(settlement, maturity, basis) + if errResult.Type == ResultTypeError { + return errResult } - return MakeNumberResult((redemption - pr) / redemption / fracResult.ValueNumber) + return MakeNumberResult((redemption - pr) / redemption / frac) } // Dollarde implements the Excel DOLLARDE function. @@ -1083,7 +1089,7 @@ func parseDollarArgs(args []Result, funcName string) (float64, float64, Result) } fraction := float64(int(args[1].ValueNumber)) if fraction < 0 { - return 0, 0, MakeErrorResultType(ErrorTypeNum, funcName + " requires fraction to be positive number") + return 0, 0, MakeErrorResultType(ErrorTypeNum, funcName + " requires fraction to be non negative number") } return dollar, fraction, MakeEmptyResult() } @@ -1134,12 +1140,12 @@ func Fv(args []Result) Result { if args[3].Type != ResultTypeNumber { return MakeErrorResult("FV requires payment to be number argument") } - pv := 0.0 + presentValue := 0.0 if argsNum >= 4 { if args[3].Type != ResultTypeNumber { return MakeErrorResult("FV requires present value to be number argument") } - pv = args[3].ValueNumber + presentValue = args[3].ValueNumber } t := 0 if argsNum == 5 { @@ -1151,7 +1157,7 @@ func Fv(args []Result) Result { t = 1 } } - return MakeNumberResult(fv(rate, nPer, pmt, pv, t)) + return MakeNumberResult(fv(rate, nPer, pmt, presentValue, t)) } // Fvschedule implements the Excel FVSCHEDULE function. @@ -1223,11 +1229,11 @@ func Intrate(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for INTRATE") } } - fracResult := yearFrac(settlement, maturity, basis) - if fracResult.Type == ResultTypeError { - return fracResult + frac, errResult := yearFrac(settlement, maturity, basis) + if errResult.Type == ResultTypeError { + return errResult } - return MakeNumberResult((redemption - investment) / investment / fracResult.ValueNumber) + return MakeNumberResult((redemption - investment) / investment / frac) } // Ipmt implements the Excel IPMT function. @@ -1271,10 +1277,15 @@ func Ipmt(args []Result) Result { return MakeErrorResult("IPMT requires start period to be number argument") } t = int(args[5].ValueNumber) - if t != 0 { + if t != 0 { t = 1 } } + + return MakeNumberResult(ipmt(rate, period, nPer, presentValue, futureValue, t)) +} + +func ipmt(rate, period, nPer, presentValue, futureValue float64, t int) float64 { payment := pmt(rate, nPer, presentValue, futureValue, t) var interest float64 if period == 1 { @@ -1290,8 +1301,7 @@ func Ipmt(args []Result) Result { interest = fv(rate, period - 1, payment, presentValue, 0) } } - - return MakeNumberResult(interest * rate) + return interest * rate } // Irr implements the Excel IRR function. @@ -1421,9 +1431,9 @@ func Ispmt(args []Result) Result { if args[3].Type != ResultTypeNumber { return MakeErrorResult("ISPMT requires present value to be number argument") } - pv := args[3].ValueNumber + presentValue := args[3].ValueNumber - return MakeNumberResult(pv * rate * (period / nPer - 1)) + return MakeNumberResult(presentValue * rate * (period / nPer - 1)) } // Mirr implements the Excel MIRR function. @@ -1521,13 +1531,13 @@ func Nper(args []Result) Result { if args[2].Type != ResultTypeNumber { return MakeErrorResult("NPER requires present value to be number argument") } - pv := args[2].ValueNumber - fv := 0.0 + presentValue := args[2].ValueNumber + futureValue := 0.0 if argsNum >= 4 { if args[3].Type != ResultTypeNumber { return MakeErrorResult("NPER requires future value to be number argument") } - fv = args[3].ValueNumber + futureValue = args[3].ValueNumber } t := 0.0 if argsNum == 5 { @@ -1539,8 +1549,8 @@ func Nper(args []Result) Result { t = 1 } } - num := pmt * (1 + rate * t) - fv * rate - den := (pv * rate + pmt * (1 + rate * t)) + num := pmt * (1 + rate * t) - futureValue * rate + den := (presentValue * rate + pmt * (1 + rate * t)) return MakeNumberResult(math.Log(num / den) / math.Log(1 + rate)) } @@ -1579,3 +1589,103 @@ func Npv(args []Result) Result { } return MakeNumberResult(npv) } + +// Pmt implements the Excel PMT function. +func Pmt(args []Result) Result { + argsNum := len(args) + if argsNum < 3 || argsNum > 5 { + return MakeErrorResult("PMT requires number of arguments in range of 3 and 5") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("PMT requires rate to be number argument") + } + rate := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("PMT requires number of periods to be number argument") + } + nPer := args[1].ValueNumber + if nPer == 0 { + return MakeErrorResultType(ErrorTypeNum, "PMT requires number of periods to be not equal to 0") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("PMT requires present value to be number argument") + } + presentValue := args[2].ValueNumber + futureValue := 0.0 + if argsNum >= 4 { + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("PMT requires future value to be number argument") + } + futureValue = args[3].ValueNumber + } + t := 0.0 + if argsNum == 5 { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("PMT requires type to be number argument") + } + t = args[4].ValueNumber + if t != 0 { + t = 1 + } + } + var result float64 + if rate == 0 { + result = (presentValue + futureValue) / nPer + } else { + term := math.Pow(1 + rate, nPer) + if t == 1 { + result = (futureValue * rate / (term - 1) + presentValue * rate / (1 - 1 / term)) / (1 + rate) + } else { + result = futureValue * rate / (term - 1) + presentValue * rate / (1 - 1 / term) + } + } + return MakeNumberResult(-result) +} + +// Ppmt implements the Excel PPPMT function. +func Ppmt(args []Result) Result { + argsNum := len(args) + if argsNum < 4 || argsNum > 6 { + return MakeErrorResult("PPMT requires number of arguments in range of 3 and 5") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("PPMT requires rate to be number argument") + } + rate := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("PPMT requires number of periods to be number argument") + } + period := args[1].ValueNumber + if period <= 0 { + return MakeErrorResultType(ErrorTypeNum, "PPMT requires period to be positive") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("PPMT requires number of periods to be number argument") + } + nPer := args[2].ValueNumber + if nPer < period { + return MakeErrorResultType(ErrorTypeNum, "PPMT requires number of periods to be not less than period") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("PPMT requires present value to be number argument") + } + presentValue := args[3].ValueNumber + futureValue := 0.0 + if argsNum >= 5 { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("PPMT requires future value to be number argument") + } + futureValue = args[4].ValueNumber + } + t := 0 + if argsNum == 6 { + if args[5].Type != ResultTypeNumber { + return MakeErrorResult("PPMT requires type to be number argument") + } + t = int(args[5].ValueNumber) + if t != 0 { + t = 1 + } + } + return MakeNumberResult(pmt(rate, nPer, presentValue, futureValue, t) - ipmt(rate, period, nPer, presentValue, futureValue, t)) +} diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index 05fa889d59..ebb32b439c 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -2192,3 +2192,42 @@ func TestNpv(t *testing.T) { runTests(t, ctx, td) } + +func TestPmt(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(0.08) + sheet.Cell("A2").SetNumber(10) + sheet.Cell("A3").SetNumber(10000) + sheet.Cell("A4").SetNumber(0.06) + sheet.Cell("A5").SetNumber(18) + sheet.Cell("A6").SetNumber(50000) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=PMT(A1/12,A2,A3)`, `-1037.0320893 ResultTypeNumber`}, + {`=PMT(A1/12,A2,A3,1)`, `-1037.1291259 ResultTypeNumber`}, + {`=PMT(A4/12,A5*12,0,A6)`, `-129.08116086 ResultTypeNumber`}, + {`=PMT("A4/12",A5*12,0,A6)`, `#VALUE! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestPpmt(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=PPMT(0.1/12,1,2*12,2000)`, `-75.623186008 ResultTypeNumber`}, + {`=PPMT(0.08,10,10,200000)`, `-27598.053462 ResultTypeNumber`}, + {`=PPMT(0.08,11,10,200000)`, `#NUM! ResultTypeError`}, + {`=PPMT("0.08%",10,10,200000)`, `#VALUE! ResultTypeError`}, + } + + runTests(t, ctx, td) +} From 3f5c932dde7df03564b0f1cba28ea06ab3a70101 Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Tue, 24 Dec 2019 19:15:16 +0300 Subject: [PATCH 02/12] PRICEDISC, fixed YEARFRAC --- spreadsheet/formula/fndatetime.go | 131 +++++++++++++------ spreadsheet/formula/fnfinance.go | 179 +++++++++++++------------- spreadsheet/formula/fnindex.go | 2 +- spreadsheet/formula/fnmathtrig.go | 2 +- spreadsheet/formula/fnstatistical.go | 2 +- spreadsheet/formula/fntext.go | 6 +- spreadsheet/formula/functions_test.go | 22 ++++ 7 files changed, 211 insertions(+), 133 deletions(-) diff --git a/spreadsheet/formula/fndatetime.go b/spreadsheet/formula/fndatetime.go index bda4f9b1a3..81629cdb87 100644 --- a/spreadsheet/formula/fndatetime.go +++ b/spreadsheet/formula/fndatetime.go @@ -31,7 +31,7 @@ func init() { RegisterFunction("TIMEVALUE", TimeValue) RegisterFunction("TODAY", Today) RegisterFunctionComplex("YEAR", Year) - RegisterFunctionComplex("YEARFRAC", YearFrac) + RegisterFunction("YEARFRAC", YearFrac) } var date1900 int64 = makeDateS(1900, time.January, 1) @@ -107,6 +107,8 @@ func initRegexpTime() { } } +var empty Result = MakeEmptyResult() + // Day is an implementation of the Excel DAY() function. func Day(args []Result) Result { if len(args) != 1 { @@ -406,7 +408,7 @@ func dateValue(dateString string) (int, int, int, bool, Result) { if !validateDate(year, month, day) { return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) } - return year, month, day, timeIsEmpty, MakeEmptyResult() + return year, month, day, timeIsEmpty, empty } func validateDate(year, month, day int) bool { @@ -743,7 +745,7 @@ func timeValue(timeString string) (int, int, float64, bool, bool, Result) { } else if hours >= 24 || seconds >= 10000 { return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } - return hours, minutes, seconds, pm, dateIsEmpty, MakeEmptyResult() + return hours, minutes, seconds, pm, dateIsEmpty, empty } // Year is an implementation of the Excel YEAR() function. @@ -760,7 +762,7 @@ func Year(ctx Context, ev Evaluator, args []Result) Result { } // YearFrac is an implementation of the Excel YEARFRAC() function. -func YearFrac(ctx Context, ev Evaluator, args []Result) Result { +func YearFrac(args []Result) Result { argsNum := len(args) if (argsNum != 2 && argsNum != 3) || args[0].Type != ResultTypeNumber || args[1].Type != ResultTypeNumber { return MakeErrorResult("YEARFRAC requires two or three number arguments") @@ -772,65 +774,92 @@ func YearFrac(ctx Context, ev Evaluator, args []Result) Result { return MakeErrorResult("YEARFRAC requires two or three number arguments") } basis = int(args[2].ValueNumber) + if !checkBasis(basis) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for YEARFRAC") + } } - epoch := ctx.GetEpoch() - startDate, err := getValueAsTime(args[0].Value(), epoch) - if err != nil { - return MakeErrorResult("incorrect start date") + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("YEARFRAC requires start date to be number argument") } - endDate, err := getValueAsTime(args[1].Value(), epoch) - if err != nil { - return MakeErrorResult("incorrect end date") + startDate := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("YEARFRAC requires end date to be number argument") } - yf, errResult := yearFracFromTime(startDate, endDate, basis) + endDate := args[1].ValueNumber + yf, errResult := yearFrac(startDate, endDate, basis) if errResult.Type == ResultTypeError { return errResult } return MakeNumberResult(yf) } -func yearFrac(startDate, endDate float64, basis int) (float64, Result) { - return yearFracFromTime(dateFromDays(startDate), dateFromDays(endDate), basis) -} - -func yearFracFromTime(startDate, endDate time.Time, basis int) (float64, Result) { +// yearFrac returns float64 fraction of the year and Result value which can be of ResultTypeError type if an error occurs or ResultTypeEmpty if doesn't. +func yearFrac(startDateF, endDateF float64, basis int) (float64, Result) { + startDate, endDate := dateFromDays(startDateF), dateFromDays(endDateF) startDateS := startDate.Unix() endDateS := endDate.Unix() - sy, sm, sd := startDate.Date() - ey, em, ed := endDate.Date() - + if startDateS == endDateS { + return 0, empty + } + sy, smM, sd := startDate.Date() + ey, emM, ed := endDate.Date() + sm, em := int(smM), int(emM) + var dayDiff, daysInYear float64 switch basis { case 0: - if sd == 31 && ed == 31 { - sd = 30 - ed = 30 - } else if sd == 31 { + if sd == 31 { + sd-- + } + if sd == 30 && ed == 31 { + ed-- + } else if leap := isLeapYear(sy); sm == 2 && ((leap && sd == 29) || (!leap && sd == 28)) { sd = 30 - } else if sd == 30 && ed == 31 { - ed = 30 + if leap := isLeapYear(ey); em == 2 && ((leap && ed == 29) || (!leap && ed == 28)) { + ed = 30 + } } - return float64(((ed + int(em) * 30 + ey * 360) - (sd + int(sm) * 30 + sy * 360))) / 360, MakeEmptyResult() + dayDiff = float64((ey - sy) * 360 + (em - sm) * 30 + (ed - sd)) + daysInYear = 360 case 1: - var ylength = 365.0 - if (sy == ey || ((sy + 1) == ey) && ((sm > em) || ((sm == em) && (sd >= ed)))) { - if ((sy == ey && isLeapYear(sy)) || feb29Between(startDate, endDate) || (em == 1 && ed == 29)) { - ylength = 366.0 + dayDiff = endDateF - startDateF + isYearDifferent := sy != ey + if isYearDifferent && (ey != sy + 1 || sm < em || (sm == em && sd < ed)) { + dayCount := 0 + for y := sy; y <= ey; y++ { + dayCount += getDaysInYear(y, 1) + } + daysInYear = float64(dayCount) / float64(ey - sy + 1) + } else { + if !isYearDifferent && isLeapYear(sy) { + daysInYear = 366 + } else { + if isYearDifferent && ((isLeapYear(sy) && (sm < 2 || (sm == 2 && sd <= 29))) || (isLeapYear(ey) && (em > 2 || (em == 2 && ed == 29)))) { + daysInYear = 366 + } else { + daysInYear = 365 + } } - return daysBetween(startDateS, endDateS) / ylength, MakeEmptyResult() } - var years = float64((ey - sy) + 1) - var days = float64((makeDateS(ey + 1, time.January, 1) - makeDateS(sy, time.January, 1)) / 86400) - var average = days / years - return daysBetween(startDateS, endDateS) / average, MakeEmptyResult() case 2: - return daysBetween(startDateS, endDateS) / 360.0, MakeEmptyResult() + dayDiff = endDateF - startDateF + daysInYear = 360 case 3: - return daysBetween(startDateS, endDateS) / 365.0, MakeEmptyResult() - case 4: - return float64(((ed + int(em) * 30 + ey * 360) - (sd + int(sm) * 30 + sy * 360))) / 360.0, MakeEmptyResult() + dayDiff = endDateF - startDateF + daysInYear = 365 + case 4: + if sd == 31 { + sd-- + } + if ed == 31 { + ed-- + } + dayDiff = float64((ey - sy) * 360 + (em - sm) * 30 + (ed - sd)) + daysInYear = 360 + default: + return 0, MakeErrorResultType(ErrorTypeNum, "Incorrect basis for YearFrac") } - return 0, MakeErrorResult("") + return dayDiff / daysInYear, empty } func getDaysInYear(year, basis int) int { @@ -983,3 +1012,23 @@ func getDaysInYearRange(from, to, basis int) int { func basis30(basis int) bool { return basis == 0 || basis == 4 } + +func parseDate(arg Result, dateName, funcName string) (float64, Result) { + var date float64 + switch arg.Type { + case ResultTypeNumber: + date = float64(int(arg.ValueNumber)) + case ResultTypeString: + dateFromString := DateValue([]Result{arg}) + if dateFromString.Type == ResultTypeError { + return 0, MakeErrorResult("Incorrect " + dateName + " date for " + funcName) + } + date = dateFromString.ValueNumber + default: + return 0, MakeErrorResult("Incorrect argument for " + funcName) + } + if date < 0 { + return 0, MakeErrorResultType(ErrorTypeNum, dateName + " should be non negative") + } + return date, empty +} diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index 89aaf04c3b..d4c7be5ab0 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -47,9 +47,25 @@ func init() { RegisterFunction("PDURATION", Pduration) RegisterFunction("PMT", Pmt) RegisterFunction("PPMT", Ppmt) + RegisterFunction("PRICEDISC", Pricedisc) RegisterFunction("_xlfn.PDURATION", Pduration) } +func getSettlementMaturity(settlementResult, maturityResult Result, funcName string) (float64, float64, Result) { + settlementDate, errResult := parseDate(settlementResult, "settlement date", funcName) + if errResult.Type == ResultTypeError { + return 0, 0, errResult + } + maturityDate, errResult := parseDate(maturityResult, "maturity date", funcName) + if errResult.Type == ResultTypeError { + return 0, 0, errResult + } + if settlementDate >= maturityDate { + return 0, 0, MakeErrorResultType(ErrorTypeNum, funcName + " requires maturity date to be later than settlement date") + } + return settlementDate, maturityDate, empty +} + // Duration implements the Excel DURATION function. func Duration(args []Result) Result { parsedArgs, err := parseDurationData(args, "DURATION") @@ -238,16 +254,9 @@ func parseCouponArgs(args []Result, funcName string) (*couponArgs, Result) { if args[0].Type != ResultTypeNumber { return nil, MakeErrorResult(funcName + " requires settlement date to be number argument") } - settlementDate := args[0].ValueNumber - if settlementDate < 0 { - return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires settlement date to be non negative") - } - if args[1].Type != ResultTypeNumber { - return nil, MakeErrorResult(funcName + " requires maturity date to be number argument") - } - maturityDate := args[1].ValueNumber - if maturityDate <= settlementDate { - return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires maturity date to be later than settlement date") + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], funcName) + if errResult.Type == ResultTypeError { + return nil, errResult } if args[2].Type != ResultTypeNumber { return nil, MakeErrorResult(funcName + " requires frequency to be number argument") @@ -271,7 +280,7 @@ func parseCouponArgs(args []Result, funcName string) (*couponArgs, Result) { maturityDate, int(freq), basis, - }, MakeEmptyResult() + }, empty } // couppcd finds last coupon date before settlement (can be equal to settlement). @@ -295,7 +304,7 @@ func coupnum(settlementDateF, maturityDateF float64, freq, basis int) (float64, if maturityDate.After(settlementDate) { aDate := couppcd(settlementDate, maturityDate, freq, basis) months := (maturityDate.Year() - aDate.Year()) * 12 + int(maturityDate.Month()) - int(aDate.Month()) - return float64(months * freq) / 12.0, MakeEmptyResult() + return float64(months * freq) / 12.0, empty } return 0, MakeErrorResultType(ErrorTypeNum, "Settlement date should be before maturity date") } @@ -348,35 +357,9 @@ func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { if len(args) != 5 && len(args) != 6 { return nil, MakeErrorResult(funcName + " requires five or six arguments") } - var settlementDate, maturityDate float64 - settlementResult := args[0] - switch settlementResult.Type { - case ResultTypeNumber: - settlementDate = float64(int(settlementResult.ValueNumber)) - case ResultTypeString: - settlementFromString := DateValue([]Result{settlementResult}) - if settlementFromString.Type == ResultTypeError { - return nil, MakeErrorResult("Incorrect settlement date for " + funcName) - } - settlementDate = settlementFromString.ValueNumber - default: - return nil, MakeErrorResult("Incorrect argument for " + funcName) - } - maturityResult := args[1] - switch maturityResult.Type { - case ResultTypeNumber: - maturityDate = float64(int(maturityResult.ValueNumber)) - case ResultTypeString: - maturityFromString := DateValue([]Result{maturityResult}) - if maturityFromString.Type == ResultTypeError { - return nil, MakeErrorResult("Incorrect maturity date for " + funcName) - } - maturityDate = maturityFromString.ValueNumber - default: - return nil, MakeErrorResult("Incorrect argument for " + funcName) - } - if settlementDate >= maturityDate { - return nil, MakeErrorResultType(ErrorTypeNum, "Settlement date should be before maturity date") + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], funcName) + if errResult.Type == ResultTypeError { + return nil, errResult } couponResult := args[2] if couponResult.Type != ResultTypeNumber { @@ -420,7 +403,7 @@ func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { yield, freq, basis, - }, MakeEmptyResult() + }, empty } func checkFreq(freq float64) bool { @@ -437,16 +420,13 @@ func Accrintm(args []Result) Result { if argsNum != 4 && argsNum != 5 { return MakeErrorResult("ACCRINTM requires four or five arguments") } - if args[0].Type != ResultTypeNumber { - return MakeErrorResult("ACCRINTM requires issue date to be number argument") - } - issue := args[0].ValueNumber - if args[1].Type != ResultTypeNumber { - return MakeErrorResult("ACCRINTM requires settlement date to be number argument") + issueDate, errResult := parseDate(args[0], "issue date", "ACCRINTM") + if errResult.Type == ResultTypeError { + return errResult } - settlement := args[1].ValueNumber - if issue >= settlement { - return MakeErrorResultType(ErrorTypeNum, "ACCRINTM requires settlement date to be later than issue date") + settlementDate, errResult := parseDate(args[1], "settlement date", "ACCRINTM") + if errResult.Type == ResultTypeError { + return errResult } if args[2].Type != ResultTypeNumber { return MakeErrorResult("ACCRINTM requires rate to be number argument") @@ -472,7 +452,7 @@ func Accrintm(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for ACCRINTM") } } - frac, errResult := yearFrac(issue, settlement, basis) + frac, errResult := yearFrac(issueDate, settlementDate, basis) if errResult.Type == ResultTypeError { return errResult } @@ -591,17 +571,14 @@ func parseAmorArgs(args []Result, funcName string) (*amorArgs, Result) { if cost < 0 { return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires cost to be non negative") } - if args[1].Type != ResultTypeNumber { - return nil, MakeErrorResult(funcName + " requires date purchased to be number argument") - } - datePurchased := args[1].ValueNumber - if datePurchased < 0 { - return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires date purchased to be non negative") + datePurchased, errResult := parseDate(args[1], "date purchased", funcName) + if errResult.Type == ResultTypeError { + return nil, errResult } - if args[2].Type != ResultTypeNumber { - return nil, MakeErrorResult(funcName + " requires first period to be number argument") + firstPeriod, errResult := parseDate(args[2], "first period", funcName) + if errResult.Type == ResultTypeError { + return nil, errResult } - firstPeriod := args[2].ValueNumber if firstPeriod < datePurchased { return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires first period to be later than date purchased") } @@ -644,7 +621,7 @@ func parseAmorArgs(args []Result, funcName string) (*amorArgs, Result) { period, rate, basis, - }, MakeEmptyResult() + }, empty } func mathRound(x float64) float64 { @@ -781,7 +758,7 @@ func parseCumulArgs(args []Result, funcName string) (*cumulArgs, Result) { startPeriod, endPeriod, t, - }, MakeEmptyResult() + }, empty } func pmt(rate, periods, present, future float64, t int ) float64 { @@ -968,16 +945,9 @@ func Disc(args []Result) Result { 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") + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "DISC") + if errResult.Type == ResultTypeError { + return errResult } if args[2].Type != ResultTypeNumber { return MakeErrorResult("DISC requires pr to be number argument") @@ -1003,7 +973,7 @@ func Disc(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for DISC") } } - frac, errResult := yearFrac(settlement, maturity, basis) + frac, errResult := yearFrac(settlementDate, maturityDate, basis) if errResult.Type == ResultTypeError { return errResult } @@ -1091,7 +1061,7 @@ func parseDollarArgs(args []Result, funcName string) (float64, float64, Result) if fraction < 0 { return 0, 0, MakeErrorResultType(ErrorTypeNum, funcName + " requires fraction to be non negative number") } - return dollar, fraction, MakeEmptyResult() + return dollar, fraction, empty } // Effect implements the Excel EFFECT function. @@ -1194,16 +1164,9 @@ func Intrate(args []Result) Result { 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") + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "INTRATE") + if errResult.Type == ResultTypeError { + return errResult } if args[2].Type != ResultTypeNumber { return MakeErrorResult("INTRATE requires investment to be number argument") @@ -1229,7 +1192,7 @@ func Intrate(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for INTRATE") } } - frac, errResult := yearFrac(settlement, maturity, basis) + frac, errResult := yearFrac(settlementDate, maturityDate, basis) if errResult.Type == ResultTypeError { return errResult } @@ -1689,3 +1652,47 @@ func Ppmt(args []Result) Result { } return MakeNumberResult(pmt(rate, nPer, presentValue, futureValue, t) - ipmt(rate, period, nPer, presentValue, futureValue, t)) } + +// Pricedisc implements the Excel PRICEDISC function. +func Pricedisc(args []Result) Result { + argsNum := len(args) + if argsNum != 4 && argsNum != 5 { + return MakeErrorResult("PRICEDISC requires four or five arguments") + } + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "PRICEDISC") + if errResult.Type == ResultTypeError { + return errResult + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("PRICEDISC requires discount of type number") + } + discount := args[2].ValueNumber + if discount <= 0 { + return MakeErrorResultType(ErrorTypeNum, "PRICEDISC requires discount to be positive") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("PRICEDISC requires redemption of type number") + } + redemption := args[3].ValueNumber + if redemption <= 0 { + return MakeErrorResultType(ErrorTypeNum, "PRICEDISC requires redemption to be positive") + } + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("PRICEDISC requires redemption to be number argument") + } + basis := 0 + if argsNum == 5 { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("PRICEDISC requires basis to be number argument") + } + basis = int(args[4].ValueNumber) + if !checkBasis(basis) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for PRICEDISC") + } + } + yf, errResult := yearFrac(settlementDate, maturityDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + return MakeNumberResult(redemption * (1 - discount * yf)) +} diff --git a/spreadsheet/formula/fnindex.go b/spreadsheet/formula/fnindex.go index 914468b0ff..7c34ada36b 100644 --- a/spreadsheet/formula/fnindex.go +++ b/spreadsheet/formula/fnindex.go @@ -547,7 +547,7 @@ func extractCol(arr Result) []Result { if len(r) > 0 { col = append(col, r[0]) } else { - col = append(col, MakeEmptyResult()) + col = append(col, empty) } } } diff --git a/spreadsheet/formula/fnmathtrig.go b/spreadsheet/formula/fnmathtrig.go index b321e0b98a..9c147c457c 100644 --- a/spreadsheet/formula/fnmathtrig.go +++ b/spreadsheet/formula/fnmathtrig.go @@ -1022,7 +1022,7 @@ func multinomial(args []Result) (float64, float64, Result) { return 0, 0, arg } } - return num, denom, MakeEmptyResult() + return num, denom, empty } // Multinomial implements the excel MULTINOMIAL function. diff --git a/spreadsheet/formula/fnstatistical.go b/spreadsheet/formula/fnstatistical.go index e94c4bf526..32075b8c8a 100644 --- a/spreadsheet/formula/fnstatistical.go +++ b/spreadsheet/formula/fnstatistical.go @@ -284,7 +284,7 @@ func checkIfsRanges(args []Result, sumRange bool, fnName string) Result { i-- // after sumRange should go column 1, not 2 } } - return MakeEmptyResult() + return empty } //getIfsMatch returns an array of indexes of cells which meets all *IFS criterias diff --git a/spreadsheet/formula/fntext.go b/spreadsheet/formula/fntext.go index ef215326d2..62916dc85a 100644 --- a/spreadsheet/formula/fntext.go +++ b/spreadsheet/formula/fntext.go @@ -198,7 +198,7 @@ func parseSearchResults(fname string, args []Result) (*parsedSearchObject, Resul findText, text, position, - }, MakeEmptyResult() + }, empty } // Find is an implementation of the Excel FIND(). @@ -543,7 +543,7 @@ func T(args []Result) Result { if s.Type == ResultTypeError || s.Type == ResultTypeString { return s } - return MakeEmptyResult() + return empty } // Trim is an implementation of the Excel TRIM function that removes leading, @@ -648,7 +648,7 @@ func parseReplaceResults(fname string, args []Result) (*parsedReplaceObject, Res startPos, length, textToReplace, - }, MakeEmptyResult() + }, empty } // Replace is an implementation of the Excel REPLACE(). diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index ebb32b439c..4022e12ac4 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -2231,3 +2231,25 @@ func TestPpmt(t *testing.T) { runTests(t, ctx, td) } + +func TestPricedisc(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetDate(time.Date(2008, 2, 16, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A2").SetDate(time.Date(2008, 3, 1, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A3").SetNumber(0.0525) + sheet.Cell("A4").SetNumber(100) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=PRICEDISC(A1,A2,A3,A4,0)`, `99.78125 ResultTypeNumber`}, + {`=PRICEDISC(A1,A2,A3,A4,1)`, `99.7991803278 ResultTypeNumber`}, + {`=PRICEDISC(A1,A2,A3,A4,2)`, `99.7958333333 ResultTypeNumber`}, + {`=PRICEDISC(A1,A2,A3,A4,3)`, `99.7986301369 ResultTypeNumber`}, + {`=PRICEDISC(A1,A2,A3,A4,4)`, `99.78125 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} From 0bc789b71d494f3ebfc3e958a075dc902fb3ebc4 Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Wed, 25 Dec 2019 13:36:58 +0300 Subject: [PATCH 03/12] PV and handling empty arguments --- spreadsheet/formula/fndatetime.go | 2 +- spreadsheet/formula/fnfinance.go | 92 ++++++++++++++++++++------- spreadsheet/formula/fnindex.go | 11 ++-- spreadsheet/formula/fntext.go | 5 +- spreadsheet/formula/functions_test.go | 80 +++++++++++++++++++++-- 5 files changed, 153 insertions(+), 37 deletions(-) diff --git a/spreadsheet/formula/fndatetime.go b/spreadsheet/formula/fndatetime.go index 81629cdb87..8e82e7e506 100644 --- a/spreadsheet/formula/fndatetime.go +++ b/spreadsheet/formula/fndatetime.go @@ -769,7 +769,7 @@ func YearFrac(args []Result) Result { } basis := 0 - if argsNum == 3 { + if argsNum == 3 && args[2].Type != ResultTypeEmpty { if args[2].Type != ResultTypeNumber { return MakeErrorResult("YEARFRAC requires two or three number arguments") } diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index d4c7be5ab0..9695da2923 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -48,6 +48,7 @@ func init() { RegisterFunction("PMT", Pmt) RegisterFunction("PPMT", Ppmt) RegisterFunction("PRICEDISC", Pricedisc) + RegisterFunction("PV", Pv) RegisterFunction("_xlfn.PDURATION", Pduration) } @@ -266,7 +267,7 @@ func parseCouponArgs(args []Result, funcName string) (*couponArgs, Result) { return nil, MakeErrorResult("Incorrect frequency for " + funcName) } basis := 0 - if argsNum == 4 { + if argsNum == 4 && args[3].Type != ResultTypeEmpty { if args[3].Type != ResultTypeNumber { return nil, MakeErrorResult(funcName + " requires basis to be number argument") } @@ -354,7 +355,8 @@ type durationArgs struct { // validateDurationData returns settlement date, maturity date, coupon rate, yield rate, frequency of payments, day count basis and error result by parsing incoming arguments func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { - if len(args) != 5 && len(args) != 6 { + argsNum := len(args) + if argsNum != 5 && argsNum != 6 { return nil, MakeErrorResult(funcName + " requires five or six arguments") } settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], funcName) @@ -386,7 +388,7 @@ func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { return nil, MakeErrorResultType(ErrorTypeNum, "Incorrect frequence value") } basis := 0 - if len(args) == 6 { + if argsNum == 6 && args[5].Type != ResultTypeEmpty { basisResult := args[5] if basisResult.Type != ResultTypeNumber { return nil, MakeErrorResult(funcName + " requires sixth argument of type number") @@ -443,7 +445,7 @@ func Accrintm(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "ACCRINTM requires par to be positive number argument") } basis := 0 - if argsNum == 5 { + if argsNum == 5 && args[4].Type != ResultTypeEmpty { if args[4].Type != ResultTypeNumber { return MakeErrorResult("ACCRINTM requires basis to be number argument") } @@ -604,7 +606,7 @@ func parseAmorArgs(args []Result, funcName string) (*amorArgs, Result) { return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires rate to be more than 0 and less than 0.5") } basis := 0 - if argsNum == 7 { + if argsNum == 7 && args[6].Type != ResultTypeEmpty { if args[6].Type != ResultTypeNumber { return nil, MakeErrorResult(funcName + " requires basis to be number argument") } @@ -829,7 +831,7 @@ func Db(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "Incorrect period for DB") } month := 12.0 - if argsNum == 5 { + if argsNum == 5 && args[4].Type != ResultTypeEmpty { if args[4].Type != ResultTypeNumber { return MakeErrorResult("DB requires month to be number argument") } @@ -904,7 +906,7 @@ func Ddb(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "Incorrect period for DDB") } factor := 2.0 - if argsNum == 5 { + if argsNum == 5 && args[4].Type != ResultTypeEmpty { if args[4].Type != ResultTypeNumber { return MakeErrorResult("DDB requires factor to be number argument") } @@ -964,7 +966,7 @@ func Disc(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "DISC requires redemption to be positive number argument") } basis := 0 - if argsNum == 5 { + if argsNum == 5 && args[4].Type != ResultTypeEmpty { if args[4].Type != ResultTypeNumber { return MakeErrorResult("DISC requires basis to be number argument") } @@ -1111,14 +1113,14 @@ func Fv(args []Result) Result { return MakeErrorResult("FV requires payment to be number argument") } presentValue := 0.0 - if argsNum >= 4 { + if argsNum >= 4 && args[3].Type != ResultTypeEmpty { if args[3].Type != ResultTypeNumber { return MakeErrorResult("FV requires present value to be number argument") } presentValue = args[3].ValueNumber } t := 0 - if argsNum == 5 { + if argsNum == 5 && args[4].Type != ResultTypeEmpty { if args[4].Type != ResultTypeNumber { return MakeErrorResult("FV requires type to be number argument") } @@ -1183,7 +1185,7 @@ func Intrate(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "INTRATE requires redemption to be positive number argument") } basis := 0 - if argsNum == 5 { + if argsNum == 5 && args[4].Type != ResultTypeEmpty { if args[4].Type != ResultTypeNumber { return MakeErrorResult("INTRATE requires basis to be number argument") } @@ -1228,14 +1230,14 @@ func Ipmt(args []Result) Result { } presentValue := args[3].ValueNumber futureValue := 0.0 - if argsNum > 4 { + if argsNum > 4 && args[4].Type != ResultTypeEmpty { 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 argsNum == 6 && args[5].Type != ResultTypeEmpty { if args[5].Type != ResultTypeNumber { return MakeErrorResult("IPMT requires start period to be number argument") } @@ -1290,7 +1292,7 @@ func Irr(args []Result) Result { return MakeErrorResultType(ErrorTypeNum, "") } guess := 0.1 - if argsNum == 2 { + if argsNum == 2 && args[1].Type != ResultTypeEmpty { if args[1].Type != ResultTypeNumber { return MakeErrorResult("IRR requires guess to be number argument") } @@ -1496,14 +1498,14 @@ func Nper(args []Result) Result { } presentValue := args[2].ValueNumber futureValue := 0.0 - if argsNum >= 4 { + if argsNum >= 4 && args[3].Type != ResultTypeEmpty { if args[3].Type != ResultTypeNumber { return MakeErrorResult("NPER requires future value to be number argument") } futureValue = args[3].ValueNumber } t := 0.0 - if argsNum == 5 { + if argsNum == 5 && args[4].Type != ResultTypeEmpty { if args[4].Type != ResultTypeNumber { return MakeErrorResult("NPER requires type to be number argument") } @@ -1575,14 +1577,14 @@ func Pmt(args []Result) Result { } presentValue := args[2].ValueNumber futureValue := 0.0 - if argsNum >= 4 { + if argsNum >= 4 && args[3].Type != ResultTypeEmpty { if args[3].Type != ResultTypeNumber { return MakeErrorResult("PMT requires future value to be number argument") } futureValue = args[3].ValueNumber } t := 0.0 - if argsNum == 5 { + if argsNum == 5 && args[4].Type != ResultTypeEmpty { if args[4].Type != ResultTypeNumber { return MakeErrorResult("PMT requires type to be number argument") } @@ -1634,14 +1636,14 @@ func Ppmt(args []Result) Result { } presentValue := args[3].ValueNumber futureValue := 0.0 - if argsNum >= 5 { + if argsNum >= 5 && args[4].Type != ResultTypeEmpty { if args[4].Type != ResultTypeNumber { return MakeErrorResult("PPMT requires future value to be number argument") } futureValue = args[4].ValueNumber } t := 0 - if argsNum == 6 { + if argsNum == 6 && args[5].Type != ResultTypeEmpty { if args[5].Type != ResultTypeNumber { return MakeErrorResult("PPMT requires type to be number argument") } @@ -1677,11 +1679,8 @@ func Pricedisc(args []Result) Result { if redemption <= 0 { return MakeErrorResultType(ErrorTypeNum, "PRICEDISC requires redemption to be positive") } - if args[4].Type != ResultTypeNumber { - return MakeErrorResult("PRICEDISC requires redemption to be number argument") - } basis := 0 - if argsNum == 5 { + if argsNum == 5 && args[4].Type != ResultTypeEmpty { if args[4].Type != ResultTypeNumber { return MakeErrorResult("PRICEDISC requires basis to be number argument") } @@ -1696,3 +1695,48 @@ func Pricedisc(args []Result) Result { } return MakeNumberResult(redemption * (1 - discount * yf)) } + +// Pv implements the Excel PV function. +func Pv(args []Result) Result { + argsNum := len(args) + if argsNum < 3 || argsNum > 5 { + return MakeErrorResult("PV requires number of arguments in range of 3 and 5") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("PV requires rate to be number argument") + } + rate := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("PV requires number of periods to be number argument") + } + nPer := args[1].ValueNumber + if nPer != float64(int(nPer)) { + return MakeErrorResultType(ErrorTypeNum, "PV requires number of periods to be integer number argument") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("PV requires payment to be number argument") + } + pmt := args[2].ValueNumber + futureValue := 0.0 + if argsNum >= 4 && args[3].Type != ResultTypeEmpty { + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("PV requires future value to be number argument") + } + futureValue = args[3].ValueNumber + } + t := 0.0 + if argsNum == 5 && args[4].Type != ResultTypeEmpty { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("PV requires type to be number argument") + } + t = args[4].ValueNumber + if t != 0 { + t = 1 + } + } + if rate == 0 { + return MakeNumberResult(-pmt * nPer - futureValue) + } else { + return MakeNumberResult((((1 - math.Pow(1 + rate, nPer)) / rate) * pmt * (1 + rate * t) - futureValue) / math.Pow(1 + rate, nPer)) + } +} diff --git a/spreadsheet/formula/fnindex.go b/spreadsheet/formula/fnindex.go index 7c34ada36b..4ab3ffce0f 100644 --- a/spreadsheet/formula/fnindex.go +++ b/spreadsheet/formula/fnindex.go @@ -98,7 +98,7 @@ func Index(args []Result) Result { } row := int(rowArg.ValueNumber) - 1 col := -1 - if argsNum == 3 { + if argsNum == 3 && args[2].Type != ResultTypeEmpty { colArg := args[2].AsNumber() if colArg.Type != ResultTypeNumber { return MakeErrorResult("INDEX requires numeric col argument") @@ -192,7 +192,7 @@ func Match(args []Result) Result { } matchType := 1 - if argsNum == 3 { + if argsNum == 3 && args[2].Type != ResultTypeEmpty { if args[2].Type != ResultTypeNumber { return MakeErrorResult("MATCH requires the third argument to be a number") } @@ -354,10 +354,11 @@ func Offset(ctx Context, ev Evaluator, args []Result) Result { // VLookup implements the VLOOKUP function that returns a matching value from a // column in an array. func VLookup(args []Result) Result { - if len(args) < 3 { + argsNum := len(args) + if argsNum < 3 { return MakeErrorResult("VLOOKUP requires at least three arguments") } - if len(args) > 4 { + if argsNum > 4 { return MakeErrorResult("VLOOKUP requires at most four arguments") } lookupValue := args[0] @@ -370,7 +371,7 @@ func VLookup(args []Result) Result { return MakeErrorResult("VLOOKUP requires numeric col argument") } exactMatch := false - if len(args) == 4 { + if argsNum == 4 && args[3].Type != ResultTypeEmpty { em := args[3].AsNumber() if em.Type != ResultTypeNumber { return MakeErrorResult("VLOOKUP requires numeric match argument") diff --git a/spreadsheet/formula/fntext.go b/spreadsheet/formula/fntext.go index 62916dc85a..899dd1c4b5 100644 --- a/spreadsheet/formula/fntext.go +++ b/spreadsheet/formula/fntext.go @@ -167,7 +167,8 @@ type parsedSearchObject struct { } func parseSearchResults(fname string, args []Result) (*parsedSearchObject, Result) { - if len(args) != 2 && len(args) != 3 { + argsNum := len(args) + if argsNum != 2 && argsNum != 3 { return nil, MakeErrorResult(fname + " requires two or three arguments") } findTextResult := args[0] @@ -181,7 +182,7 @@ func parseSearchResults(fname string, args []Result) (*parsedSearchObject, Resul text := textResult.ValueString findText := findTextResult.ValueString position := 1 - if len(args) == 3 { + if argsNum == 3 && args[2].Type != ResultTypeEmpty { positionResult := args[2] if positionResult.Type != ResultTypeNumber { return nil, MakeErrorResult("Position should be a number") diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index 4022e12ac4..c928299d31 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -533,6 +533,7 @@ func TestMatch(t *testing.T) { td := []testStruct{ {`MATCH("??ny",A1:A5)`, `2 ResultTypeNumber`}, {`MATCH("*nny",A1:A5)`, `4 ResultTypeNumber`}, + {`MATCH("*nny",A1:A5,)`, `4 ResultTypeNumber`}, {`=MATCH(5,B1:B5,1)`, `2 ResultTypeNumber`}, {`=MATCH(5,C1:C5,-1)`, `3 ResultTypeNumber`}, } @@ -947,6 +948,7 @@ func TestFind(t *testing.T) { td := []testStruct{ {`FIND("",A1)`, `1 ResultTypeNumber`}, + {`FIND("",A1,)`, `1 ResultTypeNumber`}, {`FIND(B1,A1)`, `2 ResultTypeNumber`}, {`FIND(B2,A1,3)`, `3 ResultTypeNumber`}, {`FIND(B2,A1,4)`, `#VALUE! ResultTypeError`}, @@ -975,6 +977,7 @@ func TestFindb(t *testing.T) { td := []testStruct{ {`FINDB("",A1)`, `1 ResultTypeNumber`}, {`FINDB(B1,A1)`, `2 ResultTypeNumber`}, + {`FINDB(B1,A1,)`, `2 ResultTypeNumber`}, {`FINDB(B2,A1,3)`, `3 ResultTypeNumber`}, {`FINDB(B2,A1,4)`, `#VALUE! ResultTypeError`}, {`FINDB(D1,C1)`, `1 ResultTypeNumber`}, @@ -1002,6 +1005,7 @@ func TestSearch(t *testing.T) { td := []testStruct{ {`SEARCH("",A1)`, `1 ResultTypeNumber`}, {`SEARCH(B1,A1)`, `2 ResultTypeNumber`}, + {`SEARCH(B1,A1,)`, `2 ResultTypeNumber`}, {`SEARCH(B2,A1,3)`, `3 ResultTypeNumber`}, {`SEARCH(B2,A1,4)`, `#VALUE! ResultTypeError`}, {`SEARCH(B3,A1,2)`, `2 ResultTypeNumber`}, @@ -1031,6 +1035,7 @@ func TestSearchb(t *testing.T) { td := []testStruct{ {`SEARCHB("",A1)`, `1 ResultTypeNumber`}, {`SEARCHB(B1,A1)`, `2 ResultTypeNumber`}, + {`SEARCHB(B1,A1,)`, `2 ResultTypeNumber`}, {`SEARCHB(B2,A1,3)`, `3 ResultTypeNumber`}, {`SEARCHB(B2,A1,4)`, `#VALUE! ResultTypeError`}, {`SEARCHB(D1,C1)`, `1 ResultTypeNumber`}, @@ -1096,6 +1101,7 @@ func TestYearFrac(t *testing.T) { {`=YEARFRAC(A1,A2)`, `0.00277777777 ResultTypeNumber`}, {`=YEARFRAC(A3,A4)`, `0.16666666666 ResultTypeNumber`}, {`=YEARFRAC(A3,A5)`, `0.00277777777 ResultTypeNumber`}, + {`=YEARFRAC(A3,A5,)`, `0.00277777777 ResultTypeNumber`}, {`=YEARFRAC(A1,A2,1)`, `0.00273972602 ResultTypeNumber`}, {`=YEARFRAC(A6,A7,1)`, `0.00273224043 ResultTypeNumber`}, {`=YEARFRAC(A6,A8,1)`, `1 ResultTypeNumber`}, @@ -1307,6 +1313,8 @@ func TestDuration(t *testing.T) { sheet.Cell("A8").SetString("01/01/2048") // maturity date in string format td := []testStruct{ + {`=DURATION(A1,A2,A3,A4,A5)`, `10.9191452815 ResultTypeNumber`}, + {`=DURATION(A1,A2,A3,A4,A5,)`, `10.9191452815 ResultTypeNumber`}, {`=DURATION(A1,A2,A3,A4,A5,A6)`, `10.9191452815 ResultTypeNumber`}, {`=DURATION(A7,A8,A3,A4,A5,A6)`, `10.9191452815 ResultTypeNumber`}, {`=DURATION(A1,A2,A3,A4,A5,5)`, `#NUM! ResultTypeError`}, @@ -1331,6 +1339,8 @@ func TestMduration(t *testing.T) { sheet.Cell("A8").SetString("01/01/2016") // maturity date in string format td := []testStruct{ + {`=MDURATION(A1,A2,A3,A4,A5)`, `5.73566981391 ResultTypeNumber`}, + {`=MDURATION(A1,A2,A3,A4,A5,)`, `5.73566981391 ResultTypeNumber`}, {`=MDURATION(A1,A2,A3,A4,A5,A6)`, `5.73566981391 ResultTypeNumber`}, {`=MDURATION(A7,A8,A3,A4,A5,A6)`, `5.73566981391 ResultTypeNumber`}, {`=MDURATION(A1,A2,A3,A4,A5,5)`, `#NUM! ResultTypeError`}, @@ -1424,6 +1434,7 @@ func TestVlookup(t *testing.T) { td := []testStruct{ {`=VLOOKUP(150,A1:B4,2)`, `value1 ResultTypeString`}, {`=VLOOKUP(250,A1:B4,2)`, `value2 ResultTypeString`}, + {`=VLOOKUP(250,A1:B4,2,)`, `value2 ResultTypeString`}, {`=VLOOKUP(250,A1:B4,2,FALSE)`, `#N/A ResultTypeError`}, {`=VLOOKUP(300,A1:B4,2,FALSE)`, `value3 ResultTypeString`}, } @@ -1667,11 +1678,13 @@ func TestAccrintm(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ - {`=ACCRINTM(39539,39614,0.1,1000,0))`, `20.5555555555 ResultTypeNumber`}, - {`=ACCRINTM(39539,39614,0.1,1000,1))`, `20.4918032786 ResultTypeNumber`}, - {`=ACCRINTM(39539,39614,0.1,1000,2))`, `20.8333333333 ResultTypeNumber`}, - {`=ACCRINTM(39539,39614,0.1,1000,3))`, `20.5479452054 ResultTypeNumber`}, - {`=ACCRINTM(39539,39614,0.1,1000,4))`, `20.5555555555 ResultTypeNumber`}, + {`=ACCRINTM(39539,39614,0.1,1000)`, `20.5555555555 ResultTypeNumber`}, + {`=ACCRINTM(39539,39614,0.1,1000,)`, `20.5555555555 ResultTypeNumber`}, + {`=ACCRINTM(39539,39614,0.1,1000,0)`, `20.5555555555 ResultTypeNumber`}, + {`=ACCRINTM(39539,39614,0.1,1000,1)`, `20.4918032786 ResultTypeNumber`}, + {`=ACCRINTM(39539,39614,0.1,1000,2)`, `20.8333333333 ResultTypeNumber`}, + {`=ACCRINTM(39539,39614,0.1,1000,3)`, `20.5479452054 ResultTypeNumber`}, + {`=ACCRINTM(39539,39614,0.1,1000,4)`, `20.5555555555 ResultTypeNumber`}, } runTests(t, ctx, td) @@ -1684,6 +1697,9 @@ func TestAmordegrc(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ + {`=AMORDEGRC(2400,39679,39813,300,1,0.15)`, `776 ResultTypeNumber`}, + {`=AMORDEGRC(2400,39679,39813,300,1,0.15,)`, `776 ResultTypeNumber`}, + {`=AMORDEGRC(2400,39679,39813,300,1,0.15,0)`, `776 ResultTypeNumber`}, {`=AMORDEGRC(2400,39679,39813,300,1,0.15,1)`, `776 ResultTypeNumber`}, {`=AMORDEGRC(2400,39679,39813,300,1,0.15,2)`, `#NUM! ResultTypeError`}, } @@ -1698,6 +1714,9 @@ func TestAmorlinc(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ + {`=AMORLINC(2400,39679,39813,300,1,0.15)`, `360 ResultTypeNumber`}, + {`=AMORLINC(2400,39679,39813,300,1,0.15,)`, `360 ResultTypeNumber`}, + {`=AMORLINC(2400,39679,39813,300,1,0.15,0)`, `360 ResultTypeNumber`}, {`=AMORLINC(2400,39679,39813,300,1,0.15,1)`, `360 ResultTypeNumber`}, {`=AMORLINC(2400,39679,39813,300,1,0.15,2)`, `#NUM! ResultTypeError`}, } @@ -1712,6 +1731,8 @@ func TestCoupdaybs(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ + {`=COUPDAYBS(40568,40862,2)`, `70 ResultTypeNumber`}, + {`=COUPDAYBS(40568,40862,2,)`, `70 ResultTypeNumber`}, {`=COUPDAYBS(40568,40862,2,0)`, `70 ResultTypeNumber`}, {`=COUPDAYBS(40568,40862,2,1)`, `71 ResultTypeNumber`}, {`=COUPDAYBS(40568,40862,2,2)`, `71 ResultTypeNumber`}, @@ -1740,6 +1761,8 @@ func TestCoupdays(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ + {`=COUPDAYS(40964,41228,1)`, `360 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,1,)`, `360 ResultTypeNumber`}, {`=COUPDAYS(40964,41228,1,0)`, `360 ResultTypeNumber`}, {`=COUPDAYS(40964,41228,1,1)`, `366 ResultTypeNumber`}, {`=COUPDAYS(40964,41228,1,2)`, `360 ResultTypeNumber`}, @@ -1768,6 +1791,8 @@ func TestCoupdaysnc(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ + {`=COUPDAYSNC(40933,41228,1)`, `290 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,1,)`, `290 ResultTypeNumber`}, {`=COUPDAYSNC(40933,41228,1,0)`, `290 ResultTypeNumber`}, {`=COUPDAYSNC(40933,41228,1,1)`, `295 ResultTypeNumber`}, {`=COUPDAYSNC(40933,41228,1,2)`, `295 ResultTypeNumber`}, @@ -1796,6 +1821,9 @@ func TestCoupncd(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ + {`=COUPNCD(40568,40862,1)`, `40862 ResultTypeNumber`}, + {`=COUPNCD(40568,40862,1,)`, `40862 ResultTypeNumber`}, + {`=COUPNCD(40568,40862,1,0)`, `40862 ResultTypeNumber`}, {`=COUPNCD(40568,40862,1,1)`, `40862 ResultTypeNumber`}, {`=COUPNCD(40568,40862,2,1)`, `40678 ResultTypeNumber`}, {`=COUPNCD(40568,40862,4,1)`, `40589 ResultTypeNumber`}, @@ -1812,6 +1840,9 @@ func TestCouppcd(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ + {`=COUPPCD(40568,40862,2)`, `40497 ResultTypeNumber`}, + {`=COUPPCD(40568,40862,2,)`, `40497 ResultTypeNumber`}, + {`=COUPPCD(40568,40862,2,0)`, `40497 ResultTypeNumber`}, {`=COUPPCD(40568,40862,2,1)`, `40497 ResultTypeNumber`}, {`=COUPPCD(40872,40568,2,1)`, `#NUM! ResultTypeError`}, } @@ -1826,6 +1857,9 @@ func TestCoupnum(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ + {`=COUPNUM(39107,39767,2)`, `4 ResultTypeNumber`}, + {`=COUPNUM(39107,39767,2,)`, `4 ResultTypeNumber`}, + {`=COUPNUM(39107,39767,2,0)`, `4 ResultTypeNumber`}, {`=COUPNUM(39107,39767,2,1)`, `4 ResultTypeNumber`}, {`=COUPNUM(39767,39107,2,1)`, `#NUM! ResultTypeError`}, } @@ -1925,6 +1959,8 @@ func TestDisc(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ + {`=DISC(A1,A2,A3,A4)`, `0.00068644067 ResultTypeNumber`}, + {`=DISC(A1,A2,A3,A4,)`, `0.00068644067 ResultTypeNumber`}, {`=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`}, @@ -2028,6 +2064,8 @@ func TestIntrate(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ + {`=INTRATE(A1,A2,A3,A4)`, `0.05768 ResultTypeNumber`}, + {`=INTRATE(A1,A2,A3,A4,)`, `0.05768 ResultTypeNumber`}, {`=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`}, @@ -2165,7 +2203,12 @@ func TestNper(t *testing.T) { 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,A4,)`, `60.0821228537 ResultTypeNumber`}, + {`=NPER(A1/12,A2,A3,A4,0)`, `60.0821228537 ResultTypeNumber`}, {`=NPER(A1/12,A2,A3)`, `-9.5785940398 ResultTypeNumber`}, + {`=NPER(A1/12,A2,A3,,)`, `-9.5785940398 ResultTypeNumber`}, + {`=NPER(A1/12,A2,A3,0,)`, `-9.5785940398 ResultTypeNumber`}, + {`=NPER(A1/12,A2,A3,,0)`, `-9.5785940398 ResultTypeNumber`}, } runTests(t, ctx, td) @@ -2208,6 +2251,8 @@ func TestPmt(t *testing.T) { td := []testStruct{ {`=PMT(A1/12,A2,A3)`, `-1037.0320893 ResultTypeNumber`}, + {`=PMT(A1/12,A2,A3,)`, `-1037.0320893 ResultTypeNumber`}, + {`=PMT(A1/12,A2,A3,0)`, `-1037.0320893 ResultTypeNumber`}, {`=PMT(A1/12,A2,A3,1)`, `-1037.1291259 ResultTypeNumber`}, {`=PMT(A4/12,A5*12,0,A6)`, `-129.08116086 ResultTypeNumber`}, {`=PMT("A4/12",A5*12,0,A6)`, `#VALUE! ResultTypeError`}, @@ -2244,6 +2289,8 @@ func TestPricedisc(t *testing.T) { ctx := sheet.FormulaContext() td := []testStruct{ + {`=PRICEDISC(A1,A2,A3,A4)`, `99.78125 ResultTypeNumber`}, + {`=PRICEDISC(A1,A2,A3,A4,)`, `99.78125 ResultTypeNumber`}, {`=PRICEDISC(A1,A2,A3,A4,0)`, `99.78125 ResultTypeNumber`}, {`=PRICEDISC(A1,A2,A3,A4,1)`, `99.7991803278 ResultTypeNumber`}, {`=PRICEDISC(A1,A2,A3,A4,2)`, `99.7958333333 ResultTypeNumber`}, @@ -2253,3 +2300,26 @@ func TestPricedisc(t *testing.T) { runTests(t, ctx, td) } + +func TestPv(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=PV(0.08/12,20*12,500)`, `-59777.145851 ResultTypeNumber`}, + {`=PV(0.08/12,20*12,500,,)`, `-59777.145851 ResultTypeNumber`}, + {`=PV(0.08/12,20*12,500,0,)`, `-59777.145851 ResultTypeNumber`}, + {`=PV(0.08/12,20*12,500,,0)`, `-59777.145851 ResultTypeNumber`}, + {`=PV(0.08/12,20*12,500,0,0)`, `-59777.145851 ResultTypeNumber`}, + {`=PV(0.1/12,2*12,1000,10000)`, `-29864.950264 ResultTypeNumber`}, + {`=PV(0.1/12,2*12,1000,10000,)`, `-29864.950264 ResultTypeNumber`}, + {`=PV(0.1/12,2*12,1000,10000,0)`, `-29864.950264 ResultTypeNumber`}, + {`=PV(0.1/12,2*12,1000,10000,1)`, `-30045.540721 ResultTypeNumber`}, + {`=PV(0,2*12,1000,10000,1)`, `-34000 ResultTypeNumber`}, + {`=PV("hello world",2*12,1000,10000,1)`, `#VALUE! ResultTypeError`}, + } + + runTests(t, ctx, td) +} From 27a015fc497585a3a10ae8805058b5285cac5528 Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Wed, 25 Dec 2019 17:16:56 +0300 Subject: [PATCH 04/12] RATE --- spreadsheet/formula/fnfinance.go | 73 +++++++++++++++++++++++++++ spreadsheet/formula/functions_test.go | 19 +++++++ 2 files changed, 92 insertions(+) diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index 9695da2923..3f956c7805 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -49,6 +49,7 @@ func init() { RegisterFunction("PPMT", Ppmt) RegisterFunction("PRICEDISC", Pricedisc) RegisterFunction("PV", Pv) + RegisterFunction("RATE", Rate) RegisterFunction("_xlfn.PDURATION", Pduration) } @@ -1740,3 +1741,75 @@ func Pv(args []Result) Result { return MakeNumberResult((((1 - math.Pow(1 + rate, nPer)) / rate) * pmt * (1 + rate * t) - futureValue) / math.Pow(1 + rate, nPer)) } } + +// Rate implements the Excel RATE function. +func Rate(args []Result) Result { + argsNum := len(args) + if argsNum < 3 || argsNum > 6 { + return MakeErrorResult("RATE requires number of arguments in range of 3 and 5") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("RATE requires number of periods to be number argument") + } + nPer := args[0].ValueNumber + if nPer != float64(int(nPer)) { + return MakeErrorResultType(ErrorTypeNum, "RATE requires number of periods to be integer number argument") + } + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("RATE requires payment to be number argument") + } + pmt := args[1].ValueNumber + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("RATE requires present value to be number argument") + } + presentValue := args[2].ValueNumber + futureValue := 0.0 + if argsNum >= 4 && args[3].Type != ResultTypeEmpty { + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("RATE requires future value to be number argument") + } + futureValue = args[3].ValueNumber + } + t := 0.0 + if argsNum >= 5 && args[4].Type != ResultTypeEmpty { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("RATE requires type to be number argument") + } + t = args[4].ValueNumber + if t != 0 { + t = 1 + } + } + guess := 0.1 + if argsNum >= 6 && args[5].Type != ResultTypeEmpty { + if args[5].Type != ResultTypeNumber { + return MakeErrorResult("RATE requires guess to be number argument") + } + guess = args[5].ValueNumber + } + + maxIter := 100 + iter := 0 + close := false + epsMax := 1e-6 + + rate := guess + for iter < maxIter && !close { + t1 := math.Pow(rate + 1, nPer) + t2 := math.Pow(rate + 1, nPer - 1) + rt := rate * t + 1 + p0 := pmt * (t1 - 1) + f1 := futureValue + t1 * presentValue + p0 * rt / rate + f2 := nPer * t2 * presentValue - p0 * rt / math.Pow(rate, 2) + f3 := (nPer * pmt * t2 * rt + p0 * t) / rate + + delta := f1 / (f2 + f3) + if math.Abs(delta) < epsMax { + close = true + } + iter++ + rate -= delta + } + + return MakeNumberResult(rate) +} diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index c928299d31..a761c8bc0b 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -2323,3 +2323,22 @@ func TestPv(t *testing.T) { runTests(t, ctx, td) } + +func TestRate(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=RATE(2*12,-1000,-10000,100000)`, `0.06517891177 ResultTypeNumber`}, + {`=RATE(2*12,-1000,-10000,100000,)`, `0.06517891177 ResultTypeNumber`}, + {`=RATE(2*12,-1000,-10000,100000,,)`, `0.06517891177 ResultTypeNumber`}, + {`=RATE(2*12,-1000,-10000,100000,0,0.1)`, `0.06517891177 ResultTypeNumber`}, + {`=RATE(2*12,-1000,-10000,100000,0,0.75)`, `0.06517891177 ResultTypeNumber`}, + {`=RATE(2*12,-1000,-10000,100000,0,0.065)`, `0.06517891177 ResultTypeNumber`}, + {`=RATE(2*12,-1000,-10000,100000,1,0.1)`, `0.06323958 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} From 7cb4b782f875433b15f74a6f08a80287b46f4ba7 Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Wed, 25 Dec 2019 18:13:13 +0300 Subject: [PATCH 05/12] RECEIVED --- spreadsheet/formula/fnfinance.go | 42 +++++++++++++++++++++++++++ spreadsheet/formula/functions_test.go | 24 +++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index 3f956c7805..8cd03e0fdb 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -50,6 +50,7 @@ func init() { RegisterFunction("PRICEDISC", Pricedisc) RegisterFunction("PV", Pv) RegisterFunction("RATE", Rate) + RegisterFunction("RECEIVED", Received) RegisterFunction("_xlfn.PDURATION", Pduration) } @@ -1813,3 +1814,44 @@ func Rate(args []Result) Result { return MakeNumberResult(rate) } + +// Received implements the Excel RECEIVED function. +func Received(args []Result) Result { + argsNum := len(args) + if argsNum != 4 && argsNum != 5 { + return MakeErrorResult("RECEIVED requires four or five arguments") + } + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "RECEIVED") + if errResult.Type == ResultTypeError { + return errResult + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("RECEIVED requires investment to be number argument") + } + investment := args[2].ValueNumber + if investment <= 0 { + return MakeErrorResultType(ErrorTypeNum, "RECEIVED requires investment to be positive number argument") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("RECEIVED requires discount to be number argument") + } + discount := args[3].ValueNumber + if discount <= 0 { + return MakeErrorResultType(ErrorTypeNum, "RECEIVED requires discount to be positive number argument") + } + basis := 0 + if argsNum == 5 && args[4].Type != ResultTypeEmpty { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("RECEIVED requires basis to be number argument") + } + basis = int(args[4].ValueNumber) + if !checkBasis(basis) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for RECEIVED") + } + } + frac, errResult := yearFrac(settlementDate, maturityDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + return MakeNumberResult(investment / (1 - discount * frac)) +} diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index a761c8bc0b..b6b6bd0bac 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -2342,3 +2342,27 @@ func TestRate(t *testing.T) { runTests(t, ctx, td) } + +func TestReceived(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(0.0575) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=RECEIVED(A1,A2,A3,A4)`, `1014584.6544 ResultTypeNumber`}, + {`=RECEIVED(A1,A2,A3,A4,)`, `1014584.6544 ResultTypeNumber`}, + {`=RECEIVED(A1,A2,A3,A4,0)`, `1014584.6544 ResultTypeNumber`}, + {`=RECEIVED(A1,A2,A3,A4,1)`, `1014342.13261 ResultTypeNumber`}, + {`=RECEIVED(A1,A2,A3,A4,2)`, `1014584.6544 ResultTypeNumber`}, + {`=RECEIVED(A1,A2,A3,A4,3)`, `1014381.99124 ResultTypeNumber`}, + {`=RECEIVED(A1,A2,A3,A4,4)`, `1014584.6544 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} From 8292cca2c3a109bd1f1eb5fbc649adb487531a47 Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Wed, 25 Dec 2019 18:31:34 +0300 Subject: [PATCH 06/12] RRI --- spreadsheet/formula/fnfinance.go | 32 +++++++++++++++++++++++++++ spreadsheet/formula/functions_test.go | 17 ++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index 8cd03e0fdb..a8321b67b4 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -51,6 +51,8 @@ func init() { RegisterFunction("PV", Pv) RegisterFunction("RATE", Rate) RegisterFunction("RECEIVED", Received) + RegisterFunction("RRI", Rri) + RegisterFunction("_xlfn.RRI", Rri) RegisterFunction("_xlfn.PDURATION", Pduration) } @@ -1855,3 +1857,33 @@ func Received(args []Result) Result { } return MakeNumberResult(investment / (1 - discount * frac)) } + +// Rri implements the Excel RRI function. +func Rri(args []Result) Result { + if len(args) != 3 { + return MakeErrorResult("RRI requires six arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("RRI requires number of periods to be number argument") + } + nPer := args[0].ValueNumber + if nPer <= 0 { + return MakeErrorResultType(ErrorTypeNum, "RRI requires number of periods to be positive number argument") + } + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("RRI requires present value to be number argument") + } + presentValue := args[1].ValueNumber + if presentValue <= 0 { + return MakeErrorResultType(ErrorTypeNum, "RRI requires present value to be positive number argument") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("RRI requires future value to be number argument") + } + futureValue := args[2].ValueNumber + if futureValue < 0 { + return MakeErrorResultType(ErrorTypeNum, "RRI requires future value to be non negative number argument") + } + + return MakeNumberResult(math.Pow(futureValue / presentValue, 1 / nPer) - 1) +} diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index b6b6bd0bac..709a26ce59 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -2366,3 +2366,20 @@ func TestReceived(t *testing.T) { runTests(t, ctx, td) } + +func TestRri(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=RRI(96,10000,11000)`, `0.00099330737 ResultTypeNumber`}, + {`=RRI(8,10000,11000)`, `0.01198502414 ResultTypeNumber`}, + {`=RRI(0,10000,11000)`, `#NUM! ResultTypeError`}, + {`=RRI(8,0,11000)`, `#NUM! ResultTypeError`}, + {`=RRI(8,10000,-0.000001)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} From 550e04cb3e73c86a4b24fb3b6870b37891204aa1 Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Wed, 25 Dec 2019 18:53:08 +0300 Subject: [PATCH 07/12] ODDLPRICE, ODDLYIELD --- spreadsheet/formula/fnfinance.go | 326 ++++++++++++++++++++------ spreadsheet/formula/functions_test.go | 54 +++++ 2 files changed, 311 insertions(+), 69 deletions(-) diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index a8321b67b4..1d53388e18 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -44,6 +44,8 @@ func init() { RegisterFunction("NOMINAL", Nominal) RegisterFunction("NPER", Nper) RegisterFunction("NPV", Npv) + RegisterFunction("ODDLPRICE", Oddlprice) + RegisterFunction("ODDLYIELD", Oddlyield) RegisterFunction("PDURATION", Pduration) RegisterFunction("PMT", Pmt) RegisterFunction("PPMT", Ppmt) @@ -71,72 +73,6 @@ func getSettlementMaturity(settlementResult, maturityResult Result, funcName str return settlementDate, maturityDate, empty } -// Duration implements the Excel DURATION function. -func Duration(args []Result) Result { - parsedArgs, err := parseDurationData(args, "DURATION") - if err.Type == ResultTypeError { - return err - } - settlementDate := parsedArgs.settlementDate - maturityDate := parsedArgs.maturityDate - coupon := parsedArgs.coupon - yield := parsedArgs.yield - freq := parsedArgs.freq - basis := parsedArgs.basis - - return getDuration(settlementDate, maturityDate, coupon, yield, freq, basis) -} - -// Mduration implements the Excel MDURATION function. -func Mduration(args []Result) Result { - parsedArgs, err := parseDurationData(args, "MDURATION") - if err.Type == ResultTypeError { - return err - } - settlementDate := parsedArgs.settlementDate - maturityDate := parsedArgs.maturityDate - coupon := parsedArgs.coupon - yield := parsedArgs.yield - freq := parsedArgs.freq - basis := parsedArgs.basis - - duration := getDuration(settlementDate, maturityDate, coupon, yield, freq, basis) - if duration.Type == ResultTypeError { - return duration - } - mDuration := duration.ValueNumber / (1.0 + yield / freq) - return MakeNumberResult(mDuration) -} - -// Pduration implements the Excel PDURATION function. -func Pduration(args []Result) Result { - if len(args) != 3 { - return MakeErrorResult("PDURATION requires three number arguments") - } - if args[0].Type != ResultTypeNumber { - return MakeErrorResult("PDURATION requires rate to be number argument") - } - rate := args[0].ValueNumber - if rate <= 0 { - return MakeErrorResultType(ErrorTypeNum, "PDURATION requires rate to be positive") - } - if args[1].Type != ResultTypeNumber { - return MakeErrorResult("PDURATION requires current value to be number argument") - } - currentValue := args[1].ValueNumber - if currentValue <= 0 { - return MakeErrorResultType(ErrorTypeNum, "PDURATION requires current value to be positive") - } - if args[2].Type != ResultTypeNumber { - return MakeErrorResult("PDURATION requires specified value to be number argument") - } - specifiedValue := args[2].ValueNumber - if specifiedValue <= 0 { - return MakeErrorResultType(ErrorTypeNum, "PDURATION requires specified value to be positive") - } - return MakeNumberResult((math.Log10(specifiedValue) - math.Log10(currentValue)) / math.Log10(1 + rate)) -} - type couponArgs struct { settlementDate float64 maturityDate float64 @@ -256,9 +192,6 @@ func parseCouponArgs(args []Result, funcName string) (*couponArgs, Result) { if argsNum != 3 && argsNum != 4 { return nil, MakeErrorResult(funcName + " requires four arguments") } - if args[0].Type != ResultTypeNumber { - return nil, MakeErrorResult(funcName + " requires settlement date to be number argument") - } settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], funcName) if errResult.Type == ResultTypeError { return nil, errResult @@ -1070,6 +1003,22 @@ func parseDollarArgs(args []Result, funcName string) (float64, float64, Result) return dollar, fraction, empty } +// Duration implements the Excel DURATION function. +func Duration(args []Result) Result { + parsedArgs, err := parseDurationData(args, "DURATION") + if err.Type == ResultTypeError { + return err + } + settlementDate := parsedArgs.settlementDate + maturityDate := parsedArgs.maturityDate + coupon := parsedArgs.coupon + yield := parsedArgs.yield + freq := parsedArgs.freq + basis := parsedArgs.basis + + return getDuration(settlementDate, maturityDate, coupon, yield, freq, basis) +} + // Effect implements the Excel EFFECT function. func Effect(args []Result) Result { if len(args) != 2 { @@ -1405,6 +1354,27 @@ func Ispmt(args []Result) Result { return MakeNumberResult(presentValue * rate * (period / nPer - 1)) } +// Mduration implements the Excel MDURATION function. +func Mduration(args []Result) Result { + parsedArgs, err := parseDurationData(args, "MDURATION") + if err.Type == ResultTypeError { + return err + } + settlementDate := parsedArgs.settlementDate + maturityDate := parsedArgs.maturityDate + coupon := parsedArgs.coupon + yield := parsedArgs.yield + freq := parsedArgs.freq + basis := parsedArgs.basis + + duration := getDuration(settlementDate, maturityDate, coupon, yield, freq, basis) + if duration.Type == ResultTypeError { + return duration + } + mDuration := duration.ValueNumber / (1.0 + yield / freq) + return MakeNumberResult(mDuration) +} + // Mirr implements the Excel MIRR function. func Mirr(args []Result) Result { if len(args) != 3 { @@ -1559,6 +1529,224 @@ func Npv(args []Result) Result { return MakeNumberResult(npv) } +// Oddlprice implements the Excel ODDLPRICE function. +func Oddlprice(args []Result) Result { + if len(args) != 8 && len(args) != 9 { + return MakeErrorResult("ODDLPRICE requires five or six arguments") + } + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "ODDLPRICE") + if errResult.Type == ResultTypeError { + return errResult + } + var lastInterestDate float64 + lastInterestResult := args[2] + switch lastInterestResult.Type { + case ResultTypeNumber: + lastInterestDate = float64(int(lastInterestResult.ValueNumber)) + case ResultTypeString: + lastInterestFromString := DateValue([]Result{lastInterestResult}) + if lastInterestFromString.Type == ResultTypeError { + return MakeErrorResult("Incorrect first coupon date for ODDLPRICE") + } + lastInterestDate = lastInterestFromString.ValueNumber + default: + return MakeErrorResult("Incorrect argument for ODDLPRICE") + } + + if lastInterestDate >= settlementDate { + return MakeErrorResultType(ErrorTypeNum, "Last interest date should be before settlement date") + } + rateResult := args[3] + if rateResult.Type != ResultTypeNumber { + return MakeErrorResult("ODDLPRICE requires rate of type number") + } + rate := rateResult.ValueNumber + if rate < 0 { + return MakeErrorResultType(ErrorTypeNum, "Rate should be non negative") + } + yieldResult := args[4] + if yieldResult.Type != ResultTypeNumber { + return MakeErrorResult("ODDLPRICE requires yield of type number") + } + yield := yieldResult.ValueNumber + if yield < 0 { + return MakeErrorResultType(ErrorTypeNum, "Yield should be non negative") + } + redemptionResult := args[5] + if redemptionResult.Type != ResultTypeNumber { + return MakeErrorResult("ODDLPRICE requires redemption of type number") + } + redemption := redemptionResult.ValueNumber + if redemption < 0 { + return MakeErrorResultType(ErrorTypeNum, "Yield should be non negative") + } + freqResult := args[6] + if freqResult.Type != ResultTypeNumber { + return MakeErrorResult("ODDLPRICE requires frequency of type number") + } + freq := float64(int(freqResult.ValueNumber)) + if !checkFreq(freq) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect frequence value") + } + basis := 0 + if len(args) == 8 { + basisResult := args[7] + if basisResult.Type != ResultTypeNumber { + return MakeErrorResult("ODDLPRICE requires basis of type number") + } + basis = int(basisResult.ValueNumber) + if !checkBasis(basis) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect basis value for ODDLPRICE") + } + } + + dc, errResult := yearFrac(lastInterestDate, maturityDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + dc *= freq + dsc, errResult := yearFrac(settlementDate, maturityDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + dsc *= freq + a, errResult := yearFrac(lastInterestDate, settlementDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + a *= freq + + p := redemption + dc * 100 * rate / freq + p /= dsc * yield / freq + 1 + p -= a * 100 * rate / freq + + return MakeNumberResult(p) +} + +// Oddlyield implements the Excel ODDLYIELD function. +func Oddlyield(args []Result) Result { + if len(args) != 8 && len(args) != 9 { + return MakeErrorResult("ODDLYIELD requires five or six arguments") + } + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "ODDLYIELD") + if errResult.Type == ResultTypeError { + return errResult + } + var lastInterestDate float64 + lastInterestResult := args[2] + switch lastInterestResult.Type { + case ResultTypeNumber: + lastInterestDate = float64(int(lastInterestResult.ValueNumber)) + case ResultTypeString: + lastInterestFromString := DateValue([]Result{lastInterestResult}) + if lastInterestFromString.Type == ResultTypeError { + return MakeErrorResult("Incorrect first coupon date for ODDLYIELD") + } + lastInterestDate = lastInterestFromString.ValueNumber + default: + return MakeErrorResult("Incorrect argument for ODDLYIELD") + } + + if lastInterestDate >= settlementDate { + return MakeErrorResultType(ErrorTypeNum, "Last interest date should be before settlement date") + } + rateResult := args[3] + if rateResult.Type != ResultTypeNumber { + return MakeErrorResult("ODDLYIELD requires rate of type number") + } + rate := rateResult.ValueNumber + if rate < 0 { + return MakeErrorResultType(ErrorTypeNum, "Rate should be non negative") + } + prResult := args[4] + if prResult.Type != ResultTypeNumber { + return MakeErrorResult("ODDLYIELD requires present value of type number") + } + pr := prResult.ValueNumber + if pr <= 0 { + return MakeErrorResultType(ErrorTypeNum, "Present value should be positive") + } + redemptionResult := args[5] + if redemptionResult.Type != ResultTypeNumber { + return MakeErrorResult("ODDLYIELD requires redemption of type number") + } + redemption := redemptionResult.ValueNumber + if redemption < 0 { + return MakeErrorResultType(ErrorTypeNum, "Yield should be non negative") + } + freqResult := args[6] + if freqResult.Type != ResultTypeNumber { + return MakeErrorResult("ODDLYIELD requires frequency of type number") + } + freq := float64(int(freqResult.ValueNumber)) + if !checkFreq(freq) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect frequence value") + } + basis := 0 + if len(args) == 8 { + basisResult := args[7] + if basisResult.Type != ResultTypeNumber { + return MakeErrorResult("ODDLYIELD requires basis of type number") + } + basis = int(basisResult.ValueNumber) + if !checkBasis(basis) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect basis value for ODDLYIELD") + } + } + + dc, errResult := yearFrac(lastInterestDate, maturityDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + dc *= freq + dsc, errResult := yearFrac(settlementDate, maturityDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + dsc *= freq + a, errResult := yearFrac(lastInterestDate, settlementDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + a *= freq + + yield := redemption + dc * 100 * rate / freq + yield /= pr + a * 100 * rate / freq + yield-- + yield *= freq / dsc + + return MakeNumberResult(yield) +} + +// Pduration implements the Excel PDURATION function. +func Pduration(args []Result) Result { + if len(args) != 3 { + return MakeErrorResult("PDURATION requires three number arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("PDURATION requires rate to be number argument") + } + rate := args[0].ValueNumber + if rate <= 0 { + return MakeErrorResultType(ErrorTypeNum, "PDURATION requires rate to be positive") + } + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("PDURATION requires current value to be number argument") + } + currentValue := args[1].ValueNumber + if currentValue <= 0 { + return MakeErrorResultType(ErrorTypeNum, "PDURATION requires current value to be positive") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("PDURATION requires specified value to be number argument") + } + specifiedValue := args[2].ValueNumber + if specifiedValue <= 0 { + return MakeErrorResultType(ErrorTypeNum, "PDURATION requires specified value to be positive") + } + return MakeNumberResult((math.Log10(specifiedValue) - math.Log10(currentValue)) / math.Log10(1 + rate)) +} + // Pmt implements the Excel PMT function. func Pmt(args []Result) Result { argsNum := len(args) diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index 709a26ce59..72482f225c 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -2383,3 +2383,57 @@ func TestRri(t *testing.T) { runTests(t, ctx, td) } + +func TestOddlprice(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetDate(time.Date(2008, 2, 7, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A2").SetDate(time.Date(2008, 6, 15, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A3").SetDate(time.Date(2007, 10, 15, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A4").SetNumber(0.0375) + sheet.Cell("A5").SetNumber(0.0405) + sheet.Cell("A6").SetNumber(100) + sheet.Cell("A7").SetNumber(2) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=ODDLPRICE(A1,A2,A3,A4,A5,A6,A7,0)`, `99.8782860147 ResultTypeNumber`}, + {`=ODDLPRICE(A1,A2,A3,A4,A5,A6,A7,1)`, `99.8759395207 ResultTypeNumber`}, + {`=ODDLPRICE(A1,A2,A3,A4,A5,A6,A7,2)`, `99.8769016984 ResultTypeNumber`}, + {`=ODDLPRICE(A1,A2,A3,A4,A5,A6,A7,3)`, `99.8787957508 ResultTypeNumber`}, + {`=ODDLPRICE(A1,A2,A3,A4,A5,A6,A7,4)`, `99.8782860147 ResultTypeNumber`}, + {`=ODDLPRICE(A2,A1,A3,A4,A5,A6,A7,4)`, `#NUM! ResultTypeError`}, + {`=ODDLPRICE(A1,A3,A2,A4,A5,A6,A7,4)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestOddyield(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetDate(time.Date(2008, 4, 20, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A2").SetDate(time.Date(2008, 6, 15, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A3").SetDate(time.Date(2007, 12, 24, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A4").SetNumber(0.0375) + sheet.Cell("A5").SetNumber(99.875) + sheet.Cell("A6").SetNumber(100) + sheet.Cell("A7").SetNumber(2) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=ODDLYIELD(A1,A2,A3,A4,A5,A6,A7,0)`, `0.04519223562 ResultTypeNumber`}, + {`=ODDLYIELD(A1,A2,A3,A4,A5,A6,A7,1)`, `0.04517988549 ResultTypeNumber`}, + {`=ODDLYIELD(A1,A2,A3,A4,A5,A6,A7,2)`, `0.04503841511 ResultTypeNumber`}, + {`=ODDLYIELD(A1,A2,A3,A4,A5,A6,A7,3)`, `0.04515632373 ResultTypeNumber`}, + {`=ODDLYIELD(A1,A2,A3,A4,A5,A6,A7,4)`, `0.04519223562 ResultTypeNumber`}, + {`=ODDLYIELD(A2,A1,A3,A4,A5,A6,A7,4)`, `#NUM! ResultTypeError`}, + {`=ODDLYIELD(A1,A3,A2,A4,A5,A6,A7,4)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} From 7a868f8ce4cb2eb0348f91d53cba497ad505366b Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Wed, 25 Dec 2019 19:09:42 +0300 Subject: [PATCH 08/12] PRICE, PRICEMAT --- spreadsheet/formula/fnfinance.go | 163 +++++++++++++++++++++----- spreadsheet/formula/functions_test.go | 47 ++++++++ 2 files changed, 182 insertions(+), 28 deletions(-) diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index 1d53388e18..44bc461fbb 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -49,7 +49,9 @@ func init() { RegisterFunction("PDURATION", Pduration) RegisterFunction("PMT", Pmt) RegisterFunction("PPMT", Ppmt) + RegisterFunction("PRICE", Price) RegisterFunction("PRICEDISC", Pricedisc) + RegisterFunction("PRICEMAT", Pricemat) RegisterFunction("PV", Pv) RegisterFunction("RATE", Rate) RegisterFunction("RECEIVED", Received) @@ -1538,21 +1540,10 @@ func Oddlprice(args []Result) Result { if errResult.Type == ResultTypeError { return errResult } - var lastInterestDate float64 - lastInterestResult := args[2] - switch lastInterestResult.Type { - case ResultTypeNumber: - lastInterestDate = float64(int(lastInterestResult.ValueNumber)) - case ResultTypeString: - lastInterestFromString := DateValue([]Result{lastInterestResult}) - if lastInterestFromString.Type == ResultTypeError { - return MakeErrorResult("Incorrect first coupon date for ODDLPRICE") - } - lastInterestDate = lastInterestFromString.ValueNumber - default: - return MakeErrorResult("Incorrect argument for ODDLPRICE") + lastInterestDate, errResult := parseDate(args[2], "issue date", "ODDLPRICE") + if errResult.Type == ResultTypeError { + return errResult } - if lastInterestDate >= settlementDate { return MakeErrorResultType(ErrorTypeNum, "Last interest date should be before settlement date") } @@ -1632,21 +1623,10 @@ func Oddlyield(args []Result) Result { if errResult.Type == ResultTypeError { return errResult } - var lastInterestDate float64 - lastInterestResult := args[2] - switch lastInterestResult.Type { - case ResultTypeNumber: - lastInterestDate = float64(int(lastInterestResult.ValueNumber)) - case ResultTypeString: - lastInterestFromString := DateValue([]Result{lastInterestResult}) - if lastInterestFromString.Type == ResultTypeError { - return MakeErrorResult("Incorrect first coupon date for ODDLYIELD") - } - lastInterestDate = lastInterestFromString.ValueNumber - default: - return MakeErrorResult("Incorrect argument for ODDLYIELD") + lastInterestDate, errResult := parseDate(args[2], "issue date", "ODDLPRICE") + if errResult.Type == ResultTypeError { + return errResult } - if lastInterestDate >= settlementDate { return MakeErrorResultType(ErrorTypeNum, "Last interest date should be before settlement date") } @@ -1847,6 +1827,73 @@ func Ppmt(args []Result) Result { return MakeNumberResult(pmt(rate, nPer, presentValue, futureValue, t) - ipmt(rate, period, nPer, presentValue, futureValue, t)) } +// Price implements the Excel PRICE function. +func Price(args []Result) Result { + argsNum := len(args) + if argsNum != 6 && argsNum != 7 { + return MakeErrorResult("PRICE requires six or seven arguments") + } + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "PRICE") + if errResult.Type == ResultTypeError { + return errResult + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("PRICE requires rate of type number") + } + rate := args[2].ValueNumber + if rate < 0 { + return MakeErrorResultType(ErrorTypeNum, "PRICE requires rate to not be negative") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("PRICE requires yield of type number") + } + yield := args[3].ValueNumber + if yield < 0 { + return MakeErrorResultType(ErrorTypeNum, "PRICE requires yield to not be negative") + } + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("PRICE requires redemption to be number argument") + } + redemption := args[4].ValueNumber + if redemption <= 0 { + return MakeErrorResultType(ErrorTypeNum, "PRICE requires redemption to be positive number argument") + } + freqResult := args[5] + if freqResult.Type != ResultTypeNumber { + return MakeErrorResult("PRICE requires fifth argument of type number") + } + freqF := freqResult.ValueNumber + if !checkFreq(freqF) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect frequence value") + } + freq := int(freqF) + basis := 0 + if argsNum == 7 { + if args[6].Type != ResultTypeNumber { + return MakeErrorResult("PRICE requires basis to be number argument") + } + basis = int(args[6].ValueNumber) + if !checkBasis(basis) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for PRICE") + } + } + e := coupdays(settlementDate, maturityDate, freq, basis) + dsc := coupdaysnc(settlementDate, maturityDate, freq, basis) / e + n, errResult := coupnum(settlementDate, maturityDate, freq, basis) + if errResult.Type == ResultTypeError { + return errResult + } + a := coupdaybs(settlementDate, maturityDate, freq, basis) + ret := redemption / math.Pow(1 + yield / freqF, n - 1 + dsc) + ret -= 100 * rate / freqF * a / e + t1 := 100 * rate / freqF + t2 := 1 + yield / freqF + for k := 0.0; k < n; k++ { + ret += t1 / math.Pow(t2, k + dsc) + } + return MakeNumberResult(ret) +} + // Pricedisc implements the Excel PRICEDISC function. func Pricedisc(args []Result) Result { argsNum := len(args) @@ -1888,6 +1935,66 @@ func Pricedisc(args []Result) Result { return MakeNumberResult(redemption * (1 - discount * yf)) } +// Pricemat implements the Excel PRICEMAT function. +func Pricemat(args []Result) Result { + argsNum := len(args) + if argsNum != 5 && argsNum != 6 { + return MakeErrorResult("PRICEMAT requires four or five arguments") + } + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "PRICEMAT") + if errResult.Type == ResultTypeError { + return errResult + } + issueDate, errResult := parseDate(args[2], "issue date", "PRICEMAT") + if errResult.Type == ResultTypeError { + return errResult + } + if issueDate >= settlementDate { + return MakeErrorResult("PRICEMAT requires issue date to be before settlement date") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("PRICEMAT requires rate of type number") + } + rate := args[3].ValueNumber + if rate < 0 { + return MakeErrorResultType(ErrorTypeNum, "PRICEMAT requires rate to be non negative") + } + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("PRICEMAT requires yield of type number") + } + yield := args[4].ValueNumber + if yield < 0 { + return MakeErrorResultType(ErrorTypeNum, "PRICEMAT requires yield to be non negative") + } + basis := 0 + if argsNum == 6 { + if args[5].Type != ResultTypeNumber { + return MakeErrorResult("PRICEMAT requires basis to be number argument") + } + basis = int(args[5].ValueNumber) + if !checkBasis(basis) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for PRICEMAT") + } + } + dsmyf, errResult := yearFrac(settlementDate, maturityDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + dimyf, errResult := yearFrac(issueDate, maturityDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + ayf, errResult := yearFrac(issueDate, settlementDate, basis) + if errResult.Type == ResultTypeError { + return errResult + } + + num := 1 + dimyf * rate + den := 1 + dsmyf * yield + + return MakeNumberResult((num / den - ayf * rate) * 100) +} + // Pv implements the Excel PV function. func Pv(args []Result) Result { argsNum := len(args) diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index 72482f225c..a66c156726 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -2437,3 +2437,50 @@ func TestOddyield(t *testing.T) { runTests(t, ctx, td) } + +func TestPrice(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(2017, 11, 15, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A3").SetNumber(0.0575) + sheet.Cell("A4").SetNumber(0.065) + sheet.Cell("A5").SetNumber(100) + sheet.Cell("A6").SetNumber(2) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=PRICE(A1,A2,A3,A4,A5,A6,0)`, `94.6343616213 ResultTypeNumber`}, + {`=PRICE(A1,A2,A3,A4,A5,A6,1)`, `94.6354492078 ResultTypeNumber`}, + {`=PRICE(A1,A2,A3,A4,A5,A6,2)`, `94.6024171768 ResultTypeNumber`}, + {`=PRICE(A1,A2,A3,A4,A5,A6,3)`, `94.6435945482 ResultTypeNumber`}, + {`=PRICE(A1,A2,A3,A4,A5,A6,4)`, `94.6343616213 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestPricemat(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, 4, 13, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A3").SetDate(time.Date(2007, 11, 11, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A4").SetNumber(0.061) + sheet.Cell("A5").SetNumber(0.061) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=PRICEMAT(A1,A2,A3,A4,A5,0)`, `99.9844988755 ResultTypeNumber`}, + {`=PRICEMAT(A1,A2,A3,A4,A5,1)`, `99.9802978513 ResultTypeNumber`}, + {`=PRICEMAT(A1,A2,A3,A4,A5,2)`, `99.9841690643 ResultTypeNumber`}, + {`=PRICEMAT(A1,A2,A3,A4,A5,3)`, `99.9845977645 ResultTypeNumber`}, + {`=PRICEMAT(A1,A2,A3,A4,A5,4)`, `99.9844988755 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} From 66006f09b91ade75e6eb34ef257dd9c9aa1fe460 Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Thu, 26 Dec 2019 08:35:17 +0300 Subject: [PATCH 09/12] SLN --- spreadsheet/formula/fnfinance.go | 111 +++++++++++++++----------- spreadsheet/formula/functions_test.go | 15 ++++ 2 files changed, 79 insertions(+), 47 deletions(-) diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index 44bc461fbb..700f62effa 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -47,6 +47,7 @@ func init() { RegisterFunction("ODDLPRICE", Oddlprice) RegisterFunction("ODDLYIELD", Oddlyield) RegisterFunction("PDURATION", Pduration) + RegisterFunction("_xlfn.PDURATION", Pduration) RegisterFunction("PMT", Pmt) RegisterFunction("PPMT", Ppmt) RegisterFunction("PRICE", Price) @@ -57,7 +58,7 @@ func init() { RegisterFunction("RECEIVED", Received) RegisterFunction("RRI", Rri) RegisterFunction("_xlfn.RRI", Rri) - RegisterFunction("_xlfn.PDURATION", Pduration) + RegisterFunction("SLN", Sln) } func getSettlementMaturity(settlementResult, maturityResult Result, funcName string) (float64, float64, Result) { @@ -192,7 +193,7 @@ func coupncd(settlementDate, maturityDate time.Time, freq int) time.Time { func parseCouponArgs(args []Result, funcName string) (*couponArgs, Result) { argsNum := len(args) if argsNum != 3 && argsNum != 4 { - return nil, MakeErrorResult(funcName + " requires four arguments") + return nil, MakeErrorResult(funcName + " requires three or four arguments") } settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], funcName) if errResult.Type == ResultTypeError { @@ -304,7 +305,7 @@ func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { } couponResult := args[2] if couponResult.Type != ResultTypeNumber { - return nil, MakeErrorResult(funcName + " requires third argument of type number") + return nil, MakeErrorResult(funcName + " requires coupon rate of type number") } coupon := couponResult.ValueNumber if coupon < 0 { @@ -312,7 +313,7 @@ func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { } yieldResult := args[3] if yieldResult.Type != ResultTypeNumber { - return nil, MakeErrorResult(funcName + " requires fourth argument of type number") + return nil, MakeErrorResult(funcName + " requires yield rate of type number") } yield := yieldResult.ValueNumber if yield < 0 { @@ -320,7 +321,7 @@ func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { } freqResult := args[4] if freqResult.Type != ResultTypeNumber { - return nil, MakeErrorResult(funcName + " requires fifth argument of type number") + return nil, MakeErrorResult(funcName + " requires frequency of type number") } freq := float64(int(freqResult.ValueNumber)) if !checkFreq(freq) { @@ -330,7 +331,7 @@ func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { if argsNum == 6 && args[5].Type != ResultTypeEmpty { basisResult := args[5] if basisResult.Type != ResultTypeNumber { - return nil, MakeErrorResult(funcName + " requires sixth argument of type number") + return nil, MakeErrorResult(funcName + " requires basis of type number") } basis = int(basisResult.ValueNumber) if !checkBasis(basis) { @@ -542,7 +543,7 @@ func parseAmorArgs(args []Result, funcName string) (*amorArgs, Result) { } rate := args[5].ValueNumber if rate < 0 { - return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires rate to be more than 0 and less than 0.5") + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires depreciation rate to be more than 0 and less than 0.5") } basis := 0 if argsNum == 7 && args[6].Type != ResultTypeEmpty { @@ -686,7 +687,7 @@ func parseCumulArgs(args []Result, funcName string) (*cumulArgs, Result) { return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires end period to be later or equal to start period") } if endPeriod > nPer { - return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires periods to be in number of periods range") + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires periods to be in periods range") } t := int(args[5].ValueNumber) if t != 0 && t != 1 { @@ -839,7 +840,7 @@ func Ddb(args []Result) Result { } period := args[3].ValueNumber if period < 1 { - return MakeErrorResultType(ErrorTypeNum, "DDB requires period to be positive") + return MakeErrorResultType(ErrorTypeNum, "DDB requires period to be not less than one") } if period > life { return MakeErrorResultType(ErrorTypeNum, "Incorrect period for DDB") @@ -1064,9 +1065,6 @@ func Fv(args []Result) Result { 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") - } presentValue := 0.0 if argsNum >= 4 && args[3].Type != ResultTypeEmpty { if args[3].Type != ResultTypeNumber { @@ -1111,7 +1109,7 @@ func Fvschedule(args []Result) Result { } return MakeNumberResult(principal) default: - return MakeErrorResult("FVSCHEDULE requires schedule to be of array type") + return MakeErrorResult("FVSCHEDULE requires schedule to be of number or array type") } } @@ -1160,7 +1158,7 @@ func Intrate(args []Result) Result { func Ipmt(args []Result) Result { argsNum := len(args) if argsNum < 4 || argsNum > 6 { - return MakeErrorResult("IPMT requires six arguments") + return MakeErrorResult("IPMT requires number of arguments in range between four and six") } if args[0].Type != ResultTypeNumber { return MakeErrorResult("IPMT requires rate to be number argument") @@ -1194,7 +1192,7 @@ func Ipmt(args []Result) Result { t := 0 if argsNum == 6 && args[5].Type != ResultTypeEmpty { if args[5].Type != ResultTypeNumber { - return MakeErrorResult("IPMT requires start period to be number argument") + return MakeErrorResult("IPMT requires type to be number argument") } t = int(args[5].ValueNumber) if t != 0 { @@ -1231,7 +1229,7 @@ func Irr(args []Result) Result { 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") + return MakeErrorResult("IRR requires values to be of array type") } valuesR := arrayFromRange(args[0]) values := []float64{} @@ -1331,7 +1329,7 @@ func irrResultDeriv(values, dates []float64, rate float64) float64 { // Ispmt implements the Excel ISPMT function. func Ispmt(args []Result) Result { if len(args) != 4 { - return MakeErrorResult("ISPMT requires six arguments") + return MakeErrorResult("ISPMT requires four arguments") } if args[0].Type != ResultTypeNumber { return MakeErrorResult("ISPMT requires rate to be number argument") @@ -1383,7 +1381,7 @@ func Mirr(args []Result) Result { return MakeErrorResult("MIRR requires three arguments") } if args[0].Type != ResultTypeList && args[0].Type != ResultTypeArray { - return MakeErrorResult("MIRR requires values to be range argument") + return MakeErrorResult("MIRR requires values to be of array type") } if args[1].Type != ResultTypeNumber { return MakeErrorResult("MIRR requires finance rate to be number argument") @@ -1443,7 +1441,7 @@ func Nominal(args []Result) Result { } effect := args[0].ValueNumber if effect <= 0 { - return MakeErrorResultType(ErrorTypeNum, "NOMINAL requires effect interest rate to be positive number argument") + return MakeErrorResultType(ErrorTypeNum, "NOMINAL requires effect interest rate to be positive") } if args[1].Type != ResultTypeNumber { return MakeErrorResult("NOMINAL requires number of compounding periods to be number argument") @@ -1534,7 +1532,7 @@ func Npv(args []Result) Result { // Oddlprice implements the Excel ODDLPRICE function. func Oddlprice(args []Result) Result { if len(args) != 8 && len(args) != 9 { - return MakeErrorResult("ODDLPRICE requires five or six arguments") + return MakeErrorResult("ODDLPRICE requires eight or nine arguments") } settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "ODDLPRICE") if errResult.Type == ResultTypeError { @@ -1616,8 +1614,8 @@ func Oddlprice(args []Result) Result { // Oddlyield implements the Excel ODDLYIELD function. func Oddlyield(args []Result) Result { - if len(args) != 8 && len(args) != 9 { - return MakeErrorResult("ODDLYIELD requires five or six arguments") + if len(args) != 7 && len(args) != 8 { + return MakeErrorResult("ODDLYIELD requires seven or eight arguments") } settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "ODDLYIELD") if errResult.Type == ResultTypeError { @@ -1630,45 +1628,40 @@ func Oddlyield(args []Result) Result { if lastInterestDate >= settlementDate { return MakeErrorResultType(ErrorTypeNum, "Last interest date should be before settlement date") } - rateResult := args[3] - if rateResult.Type != ResultTypeNumber { + if args[3].Type != ResultTypeNumber { return MakeErrorResult("ODDLYIELD requires rate of type number") } - rate := rateResult.ValueNumber + rate := args[3].ValueNumber if rate < 0 { return MakeErrorResultType(ErrorTypeNum, "Rate should be non negative") } - prResult := args[4] - if prResult.Type != ResultTypeNumber { + if args[4].Type != ResultTypeNumber { return MakeErrorResult("ODDLYIELD requires present value of type number") } - pr := prResult.ValueNumber + pr := args[4].ValueNumber if pr <= 0 { return MakeErrorResultType(ErrorTypeNum, "Present value should be positive") } - redemptionResult := args[5] - if redemptionResult.Type != ResultTypeNumber { + if args[5].Type != ResultTypeNumber { return MakeErrorResult("ODDLYIELD requires redemption of type number") } - redemption := redemptionResult.ValueNumber + redemption := args[5].ValueNumber if redemption < 0 { return MakeErrorResultType(ErrorTypeNum, "Yield should be non negative") } - freqResult := args[6] - if freqResult.Type != ResultTypeNumber { + if args[6].Type != ResultTypeNumber { return MakeErrorResult("ODDLYIELD requires frequency of type number") } - freq := float64(int(freqResult.ValueNumber)) + freq := float64(int(args[6].ValueNumber)) if !checkFreq(freq) { return MakeErrorResultType(ErrorTypeNum, "Incorrect frequence value") } basis := 0 if len(args) == 8 { - basisResult := args[7] - if basisResult.Type != ResultTypeNumber { + if args[7].Type != ResultTypeNumber { return MakeErrorResult("ODDLYIELD requires basis of type number") } - basis = int(basisResult.ValueNumber) + basis = int(args[7].ValueNumber) if !checkBasis(basis) { return MakeErrorResultType(ErrorTypeNum, "Incorrect basis value for ODDLYIELD") } @@ -1701,7 +1694,7 @@ func Oddlyield(args []Result) Result { // Pduration implements the Excel PDURATION function. func Pduration(args []Result) Result { if len(args) != 3 { - return MakeErrorResult("PDURATION requires three number arguments") + return MakeErrorResult("PDURATION requires three arguments") } if args[0].Type != ResultTypeNumber { return MakeErrorResult("PDURATION requires rate to be number argument") @@ -1783,14 +1776,14 @@ func Pmt(args []Result) Result { func Ppmt(args []Result) Result { argsNum := len(args) if argsNum < 4 || argsNum > 6 { - return MakeErrorResult("PPMT requires number of arguments in range of 3 and 5") + return MakeErrorResult("PPMT requires number of arguments in range of four and six") } if args[0].Type != ResultTypeNumber { return MakeErrorResult("PPMT requires rate to be number argument") } rate := args[0].ValueNumber if args[1].Type != ResultTypeNumber { - return MakeErrorResult("PPMT requires number of periods to be number argument") + return MakeErrorResult("PPMT requires period to be number argument") } period := args[1].ValueNumber if period <= 0 { @@ -1860,7 +1853,7 @@ func Price(args []Result) Result { } freqResult := args[5] if freqResult.Type != ResultTypeNumber { - return MakeErrorResult("PRICE requires fifth argument of type number") + return MakeErrorResult("PRICE requires frequency of type number") } freqF := freqResult.ValueNumber if !checkFreq(freqF) { @@ -1939,7 +1932,7 @@ func Pricedisc(args []Result) Result { func Pricemat(args []Result) Result { argsNum := len(args) if argsNum != 5 && argsNum != 6 { - return MakeErrorResult("PRICEMAT requires four or five arguments") + return MakeErrorResult("PRICEMAT requires five or six arguments") } settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "PRICEMAT") if errResult.Type == ResultTypeError { @@ -2044,7 +2037,7 @@ func Pv(args []Result) Result { func Rate(args []Result) Result { argsNum := len(args) if argsNum < 3 || argsNum > 6 { - return MakeErrorResult("RATE requires number of arguments in range of 3 and 5") + return MakeErrorResult("RATE requires number of arguments in range of three and six") } if args[0].Type != ResultTypeNumber { return MakeErrorResult("RATE requires number of periods to be number argument") @@ -2156,29 +2149,53 @@ func Received(args []Result) Result { // Rri implements the Excel RRI function. func Rri(args []Result) Result { if len(args) != 3 { - return MakeErrorResult("RRI requires six arguments") + return MakeErrorResult("RRI requires three arguments") } if args[0].Type != ResultTypeNumber { return MakeErrorResult("RRI requires number of periods to be number argument") } nPer := args[0].ValueNumber if nPer <= 0 { - return MakeErrorResultType(ErrorTypeNum, "RRI requires number of periods to be positive number argument") + return MakeErrorResultType(ErrorTypeNum, "RRI requires number of periods to be positive") } if args[1].Type != ResultTypeNumber { return MakeErrorResult("RRI requires present value to be number argument") } presentValue := args[1].ValueNumber if presentValue <= 0 { - return MakeErrorResultType(ErrorTypeNum, "RRI requires present value to be positive number argument") + return MakeErrorResultType(ErrorTypeNum, "RRI requires present value to be positive") } if args[2].Type != ResultTypeNumber { return MakeErrorResult("RRI requires future value to be number argument") } futureValue := args[2].ValueNumber if futureValue < 0 { - return MakeErrorResultType(ErrorTypeNum, "RRI requires future value to be non negative number argument") + return MakeErrorResultType(ErrorTypeNum, "RRI requires future value to be non negative") } return MakeNumberResult(math.Pow(futureValue / presentValue, 1 / nPer) - 1) } + +// Sln implements the Excel SLN function. +func Sln(args []Result) Result { + if len(args) != 3 { + return MakeErrorResult("SLN requires three arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("SLN requires cost to be number argument") + } + cost := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("SLN requires salvage to be number argument") + } + salvage := args[1].ValueNumber + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("SLN requires life to be number argument") + } + life := args[2].ValueNumber + if life == 0 { + return MakeErrorResultType(ErrorTypeDivideByZero, "SLN requires life to be non zero") + } + + return MakeNumberResult((cost - salvage ) / life) +} diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index a66c156726..252f5cb330 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -2484,3 +2484,18 @@ func TestPricemat(t *testing.T) { runTests(t, ctx, td) } + +func TestSln(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=SLN(30000,7500,10)`, `2250 ResultTypeNumber`}, + {`=SLN(30000,7500,0)`, `#DIV/0! ResultTypeError`}, + {`=SLN("hello world",7500,10)`, `#VALUE! ResultTypeError`}, + } + + runTests(t, ctx, td) +} From 1d002fb5a448f6cb2dbd54b5d4c2b8ddcd66835d Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Thu, 26 Dec 2019 08:50:02 +0300 Subject: [PATCH 10/12] SYD --- spreadsheet/formula/fnfinance.go | 38 +++++++++++++++++++++++++++ spreadsheet/formula/functions_test.go | 18 +++++++++++++ 2 files changed, 56 insertions(+) diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index 700f62effa..c07e028c79 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -59,6 +59,7 @@ func init() { RegisterFunction("RRI", Rri) RegisterFunction("_xlfn.RRI", Rri) RegisterFunction("SLN", Sln) + RegisterFunction("SYD", Syd) } func getSettlementMaturity(settlementResult, maturityResult Result, funcName string) (float64, float64, Result) { @@ -2199,3 +2200,40 @@ func Sln(args []Result) Result { return MakeNumberResult((cost - salvage ) / life) } + +// Syd implements the Excel SYD function. +func Syd(args []Result) Result { + if len(args) != 4 { + return MakeErrorResult("SYD requires three arguments") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("SYD requires cost to be number argument") + } + cost := args[0].ValueNumber + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("SYD requires salvage to be number argument") + } + salvage := args[1].ValueNumber + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("SYD requires life to be number argument") + } + life := args[2].ValueNumber + if life <= 0 { + return MakeErrorResultType(ErrorTypeNum, "SYD requires life to be positive") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("SYD requires period to be number argument") + } + per := args[3].ValueNumber + if per <= 0 { + return MakeErrorResultType(ErrorTypeNum, "SYD requires period to be positive") + } + if per > life { + return MakeErrorResultType(ErrorTypeNum, "SYD requires period to be equal or less than life") + } + + num := (cost - salvage) * (life - per + 1) * 2 + den := life * (life + 1) + + return MakeNumberResult(num / den) +} diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index 252f5cb330..7f67b7ba55 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -2499,3 +2499,21 @@ func TestSln(t *testing.T) { runTests(t, ctx, td) } + +func TestSyd(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=SYD(30000,7500,10,1)`, `4090.9090909 ResultTypeNumber`}, + {`=SYD(30000,7500,10,10)`, `409.09090909 ResultTypeNumber`}, + {`=SYD(30000,7500,10,11)`, `#NUM! ResultTypeError`}, + {`=SYD(30000,7500,0,0)`, `#NUM! ResultTypeError`}, + {`=SYD(30000,7500,10,0)`, `#NUM! ResultTypeError`}, + {`=SLN("hello world",7500,10,1)`, `#VALUE! ResultTypeError`}, + } + + runTests(t, ctx, td) +} From 384f5eb743ecb5433fe8b74bf67190ebb07c3607 Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Thu, 26 Dec 2019 14:02:56 +0300 Subject: [PATCH 11/12] TBILLEQ, TBILLPRICE, TBILLYIELD --- spreadsheet/formula/fnfinance.go | 74 +++++++++++++++++++++++++++ spreadsheet/formula/functions_test.go | 66 ++++++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index c07e028c79..f2e19fe73c 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -60,6 +60,9 @@ func init() { RegisterFunction("_xlfn.RRI", Rri) RegisterFunction("SLN", Sln) RegisterFunction("SYD", Syd) + RegisterFunction("TBILLEQ", Tbilleq) + RegisterFunction("TBILLPRICE", Tbillprice) + RegisterFunction("TBILLYIELD", Tbillyield) } func getSettlementMaturity(settlementResult, maturityResult Result, funcName string) (float64, float64, Result) { @@ -2237,3 +2240,74 @@ func Syd(args []Result) Result { return MakeNumberResult(num / den) } + +// Tbilleq implements the Excel TBILLEQ function. +func Tbilleq(args []Result) Result { + if len(args) != 3 { + return MakeErrorResult("TBILLEQ requires three arguments") + } + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "TBILLEQ") + if errResult.Type == ResultTypeError { + return errResult + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("TBILLEQ requires discount to be number argument") + } + dsm := maturityDate - settlementDate + if dsm > 365 { + return MakeErrorResultType(ErrorTypeNum, "TBILLEQ requires maturity to be not more than one year after settlement") + } + discount := args[2].ValueNumber + if discount <= 0 { + return MakeErrorResultType(ErrorTypeNum, "TBILLEQ requires discount to be positive number argument") + } + return MakeNumberResult((365 * discount) / (360 - discount * dsm)) +} + +// Tbillprice implements the Excel TBILLPRICE function. +func Tbillprice(args []Result) Result { + if len(args) != 3 { + return MakeErrorResult("TBILLPRICE requires three arguments") + } + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "TBILLPRICE") + if errResult.Type == ResultTypeError { + return errResult + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("TBILLPRICE requires discount to be number argument") + } + dsm := maturityDate - settlementDate + if dsm > 365 { + return MakeErrorResultType(ErrorTypeNum, "TBILLPRICE requires maturity to be not more than one year after settlement") + } + discount := args[2].ValueNumber + if discount <= 0 { + return MakeErrorResultType(ErrorTypeNum, "TBILLPRICE requires discount to be positive number argument") + } + return MakeNumberResult(100 * (1 - discount * dsm / 360)) +} + +// Tbillyield implements the Excel TBILLYIELD function. +func Tbillyield(args []Result) Result { + if len(args) != 3 { + return MakeErrorResult("TBILLYIELD requires three arguments") + } + settlementDate, maturityDate, errResult := getSettlementMaturity(args[0], args[1], "TBILLYIELD") + if errResult.Type == ResultTypeError { + return errResult + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("TBILLYIELD requires discount to be number argument") + } + dsm := maturityDate - settlementDate + if dsm > 365 { + return MakeErrorResultType(ErrorTypeNum, "TBILLYIELD requires maturity to be not more than one year after settlement") + } + pr := args[2].ValueNumber + if pr <= 0 { + return MakeErrorResultType(ErrorTypeNum, "TBILLYIELD requires pr to be positive number argument") + } + m1 := (100 - pr) / pr + m2 := 360 /dsm + return MakeNumberResult(m1 * m2) +} diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index 7f67b7ba55..30d06d46f7 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -2517,3 +2517,69 @@ func TestSyd(t *testing.T) { runTests(t, ctx, td) } + +func TestTbilleq(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetDate(time.Date(2008, 3, 31, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A2").SetDate(time.Date(2008, 6, 1, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A3").SetNumber(0.0914) + sheet.Cell("A4").SetDate(time.Date(2009, 4, 1, 0, 0, 0, 0, time.UTC)) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=TBILLEQ(A1,A2,A3)`, `0.09415149356 ResultTypeNumber`}, + {`=TBILLEQ("A1",A2,A3)`, `#VALUE! ResultTypeError`}, + {`=TBILLEQ(A1,A2,0)`, `#NUM! ResultTypeError`}, + {`=TBILLEQ(A2,A1,A3)`, `#NUM! ResultTypeError`}, + {`=TBILLEQ(A1,A4,A3)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestTbillprice(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetDate(time.Date(2008, 3, 31, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A2").SetDate(time.Date(2008, 6, 1, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A3").SetNumber(0.09) + sheet.Cell("A4").SetDate(time.Date(2009, 4, 1, 0, 0, 0, 0, time.UTC)) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=TBILLPRICE(A1,A2,A3)`, `98.45 ResultTypeNumber`}, + {`=TBILLPRICE("A1",A2,A3)`, `#VALUE! ResultTypeError`}, + {`=TBILLPRICE(A1,A2,0)`, `#NUM! ResultTypeError`}, + {`=TBILLPRICE(A2,A1,A3)`, `#NUM! ResultTypeError`}, + {`=TBILLPRICE(A1,A4,A3)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestTbillyield(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetDate(time.Date(2008, 3, 31, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A2").SetDate(time.Date(2008, 6, 1, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A3").SetNumber(98.45) + sheet.Cell("A4").SetDate(time.Date(2009, 4, 1, 0, 0, 0, 0, time.UTC)) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=TBILLYIELD(A1,A2,A3)`, `0.09141696292 ResultTypeNumber`}, + {`=TBILLYIELD("A1",A2,A3)`, `#VALUE! ResultTypeError`}, + {`=TBILLYIELD(A1,A2,0)`, `#NUM! ResultTypeError`}, + {`=TBILLYIELD(A2,A1,A3)`, `#NUM! ResultTypeError`}, + {`=TBILLYIELD(A1,A4,A3)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} From 1c5b5203785c77d6c4014d5413b92afe9abe4bbb Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Fri, 27 Dec 2019 09:44:27 +0300 Subject: [PATCH 12/12] VDB --- spreadsheet/formula/fnfinance.go | 192 +++++++++++++++++++++++--- spreadsheet/formula/functions_test.go | 26 ++++ 2 files changed, 195 insertions(+), 23 deletions(-) diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index f2e19fe73c..7b5d73174f 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -63,6 +63,7 @@ func init() { RegisterFunction("TBILLEQ", Tbilleq) RegisterFunction("TBILLPRICE", Tbillprice) RegisterFunction("TBILLYIELD", Tbillyield) + RegisterFunction("VDB", Vdb) } func getSettlementMaturity(settlementResult, maturityResult Result, funcName string) (float64, float64, Result) { @@ -860,29 +861,7 @@ func Ddb(args []Result) Result { } } - 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) + return MakeNumberResult(getDDB(cost, salvage, life, period, factor)) } // Disc implements the Excel DISC function. @@ -2311,3 +2290,170 @@ func Tbillyield(args []Result) Result { m2 := 360 /dsm return MakeNumberResult(m1 * m2) } + +// Vdb implements the Excel VDB function. +func Vdb(args []Result) Result { + argsNum := len(args) + if argsNum < 5 || argsNum > 7 { + return MakeErrorResult("VDB requires number of arguments to be in range between five and seven") + } + if args[0].Type != ResultTypeNumber { + return MakeErrorResult("VDB requires cost to be number argument") + } + cost := args[0].ValueNumber + if cost < 0 { + return MakeErrorResultType(ErrorTypeNum, "VDB requires cost to be non negative") + } + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("VDB requires salvage to be number argument") + } + salvage := args[1].ValueNumber + if salvage < 0 { + return MakeErrorResultType(ErrorTypeNum, "VDB requires salvage to be non negative") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("VDB requires life to be number argument") + } + life := args[2].ValueNumber + if life == 0 { + return MakeErrorResultType(ErrorTypeDivideByZero, "VDB requires life to be positive") + } + if life < 0 { + return MakeErrorResultType(ErrorTypeNum, "VDB requires life to be positive") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("VDB requires start period to be number argument") + } + startPeriod := args[3].ValueNumber + if startPeriod < 0 { + return MakeErrorResultType(ErrorTypeNum, "VDB requires start period to be not less than one") + } + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("VDB requires end period to be number argument") + } + endPeriod := args[4].ValueNumber + if startPeriod > endPeriod { + return MakeErrorResultType(ErrorTypeNum, "Incorrect start period for VDB") + } + if endPeriod > life { + return MakeErrorResultType(ErrorTypeNum, "Incorrect end period for VDB") + } + factor := 2.0 + if argsNum > 5 { + if args[5].Type == ResultTypeEmpty { + factor = 0.0 + } else { + if args[5].Type != ResultTypeNumber { + return MakeErrorResult("VDB requires factor to be number argument") + } + factor = args[5].ValueNumber + if factor < 0 { + return MakeErrorResultType(ErrorTypeNum, "VDB requires factor to be non negative") + } + } + } + noSwitch := false + if argsNum > 6 && args[6].Type != ResultTypeEmpty { + if args[6].Type != ResultTypeNumber { + return MakeErrorResult("VDB requires no_switch to be number argument") + } + noSwitch = args[6].ValueNumber != 0 + } + + vdb := 0.0 + startInt := math.Floor(startPeriod) + endInt := math.Ceil(endPeriod) + + if noSwitch { + for i := startInt + 1; i <= endInt; i++ { + term := getDDB(cost, salvage, life, i, factor) + if i == startInt + 1 { + term *= math.Min(endPeriod, startInt + 1) - startPeriod + } else if i == endInt { + term *= endPeriod + 1 - endInt + } + vdb += term + } + } else { + life1 := life + var part float64 + if !approxEqual(startPeriod, math.Floor(startPeriod)) { + if factor == 1 { + l2 := life / 2 + if startPeriod > l2 || approxEqual(startPeriod, l2) { + part = startPeriod - l2 + startPeriod = l2 + endPeriod -= part + life1++ + } + } + } + if factor != 0 { + cost -= interVDB(cost, salvage, life, life1, startPeriod, factor) + } + vdb = interVDB(cost, salvage, life, life - startPeriod, endPeriod - startPeriod, factor) + } + return MakeNumberResult(vdb) +} + +func interVDB(cost, salvage, life, life1, period, factor float64) float64 { + var ddb, term float64 + vdb := 0.0 + endInt := math.Ceil(period) + cs := cost - salvage + nowSln := false + sln := 0.0 + + for i := 1.0; i <= endInt; i++ { + if !nowSln { + ddb = getDDB(cost, salvage, life, i, factor) + sln = cs / (life - i + 1) + if sln > ddb { + term = sln + nowSln = true + } else { + term = ddb + cs -= ddb + } + } else { + term = sln + } + if i == endInt { + term *= period + 1 - endInt + } + vdb += term + } + return vdb +} + +func getDDB(cost, salvage, life, period, factor float64) float64 { + var oldValue float64 + rate := factor / life + if rate >= 1 { + rate = 1 + if period == 1 { + oldValue = cost + } else { + oldValue = 0 + } + } 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 ddb +} + +func approxEqual(a, b float64) bool { + return math.Abs(a - b) < 1.0e-6 +} diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index 30d06d46f7..3ee3efc232 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -2583,3 +2583,29 @@ func TestTbillyield(t *testing.T) { runTests(t, ctx, td) } + +func TestVdb(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{ + {`=VDB(A1,A2,A3*365,0,1)`, `1.31506849315 ResultTypeNumber`}, + {`=VDB(A1,A2,A3*12,0,1)`, `40 ResultTypeNumber`}, + {`=VDB(A1,A2,A3,0,1)`, `480 ResultTypeNumber`}, + {`=VDB(A1,A2,A3*12,6,18)`, `396.306053264 ResultTypeNumber`}, + {`=VDB(A1,A2,A3*12,6,18,)`, `210 ResultTypeNumber`}, + {`=VDB(A1,A2,A3*12,6,18,,1)`, `0 ResultTypeNumber`}, + {`=VDB(A1,A2,A3*12,6,18,1.5)`, `311.808936658 ResultTypeNumber`}, + {`=VDB(A1,A2,A3,0,0.875,1.5)`, `315 ResultTypeNumber`}, + {`=VDB(A1,A2,A3,0,0.875,,)`, `183.75 ResultTypeNumber`}, + {`=VDB(A1,A2,A3,0,0.875,,1)`, `0 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +}