diff --git a/spreadsheet/formula/fndatetime.go b/spreadsheet/formula/fndatetime.go index 92c1fc8d..479ee103 100644 --- a/spreadsheet/formula/fndatetime.go +++ b/spreadsheet/formula/fndatetime.go @@ -416,10 +416,14 @@ func validateDate(year, month, day int) bool { if day < 1 { return false } + return day <= getDaysInMonth(year, month) +} + +func getDaysInMonth(year, month int) int { if month == 2 && isLeapYear(year) { - return day <= 29 + return 29 } else { - return day <= daysInMonth[month-1] + return daysInMonth[month-1] } } @@ -779,10 +783,14 @@ func YearFrac(ctx Context, ev Evaluator, args []Result) Result { if err != nil { return MakeErrorResult("incorrect end date") } - return yearFrac(startDate, endDate, basis) + return yearFracFromTime(startDate, endDate, basis) } -func yearFrac(startDate, endDate time.Time, basis int) Result { +func yearFrac(startDate, endDate float64, basis int) Result { + return yearFracFromTime(dateFromDays(startDate), dateFromDays(endDate), basis) +} + +func yearFracFromTime(startDate, endDate time.Time, basis int) Result { startDateS := startDate.Unix() endDateS := endDate.Unix() sy, sm, sd := startDate.Date() @@ -821,6 +829,21 @@ func yearFrac(startDate, endDate time.Time, basis int) Result { return MakeErrorResultType(ErrorTypeValue, "") } +func getDaysInYear(year, basis int) int { + switch basis { + case 1: + if isLeapYear(year) { + return 366 + } else { + return 365 + } + case 3: + return 365 + default: + return 360 + } +} + func makeDateS(y int, m time.Month, d int) int64 { if y == 1900 && int(m) <= 2 { d-- @@ -855,3 +878,104 @@ func feb29Between(date1, date2 time.Time) bool { var mar1year2 = makeDateS(year2, time.March, 1) return (isLeapYear(year2) && date2S >= mar1year2 && date1S < mar1year2) } + +func getDiff(from, to time.Time, basis int) float64 { + if from.After(to) { + from, to = to, from + } + diff := 0 + + yFrom, mFromM, dFromOrig := from.Date() + yTo, mToM, dToOrig := to.Date() + + mFrom, mTo := int(mFromM), int(mToM) + dFrom, dTo := getDayOnBasis(yFrom, mFrom, dFromOrig, basis), getDayOnBasis(yTo, mTo, dToOrig, basis) + + if !basis30(basis) { + return daysFromDate(yTo, mTo, dTo) - daysFromDate(yFrom, mFrom, dFrom) + } + + if basis == 0 { + if (mFrom == 2 || dFrom < 30 ) && dToOrig == 31 { + dTo = 31 + } else if mTo == 2 && dTo == getDaysInMonth(yTo, mTo) { + dTo = getDaysInMonth(yTo, 2) + } + } else { + if mFrom == 2 && dFrom == 30 { + dFrom = getDaysInMonth(yFrom, 2) + } + if mTo == 2 && dTo == 30 { + dTo = getDaysInMonth(yTo, 2) + } + } + + if yFrom < yTo || (yFrom == yTo && mFrom < mTo) { + diff = 30 - dFrom + 1 + dFromOrig = 1 + dFrom = 1 + fromNew := time.Date(yFrom, time.Month(mFrom), dFromOrig, 0, 0, 0, 0, time.UTC).AddDate(0,1,0) + if fromNew.Year() < yTo { + diff += getDaysInMonthRange(fromNew.Year(), int(fromNew.Month()), 12, basis) + fromNew = fromNew.AddDate(0, 13 - int(fromNew.Month()), 0) + diff += getDaysInYearRange(fromNew.Year(), yTo - 1, basis) + } + diff += getDaysInMonthRange(yTo, int(fromNew.Month()), mTo - 1, basis) + fromNew = fromNew.AddDate(0, mTo - int(fromNew.Month()), 0) + mFrom = fromNew.Day() + } + diff += dTo - dFrom + if diff > 0 { + return float64(diff) + } else { + return 0 + } +} + +func getDayOnBasis(year, month, dayOrig, basis int) int { + if !basis30(basis) { + return dayOrig + } + day := dayOrig + dim := getDaysInMonth(year, month) + if day > 30 || dayOrig >= dim || day >= dim { + day = 30 + } + return day +} + +func getDaysInMonthRange(y, from, to, basis int) int { + if from > to { + return 0 + } + if basis30(basis) { + return (to - from + 1) * 30 + } + days := 0 + for m := from; m <= to; m++ { + days += getDaysInMonth(y, m) + } + return days +} + +func getDaysInYearRange(from, to, basis int) int { + if from > to { + return 0 + } + if basis30(basis) { + return (to - from + 1) * 360 + } + days := 0 + for y := from; y <= to; y++ { + dy := 365 + if isLeapYear(y) { + dy = 366 + } + days += dy + } + return days +} + +func basis30(basis int) bool { + return basis == 0 || basis == 4 +} diff --git a/spreadsheet/formula/fnfinance.go b/spreadsheet/formula/fnfinance.go index 78ec858e..492a948f 100644 --- a/spreadsheet/formula/fnfinance.go +++ b/spreadsheet/formula/fnfinance.go @@ -17,24 +17,49 @@ func init() { RegisterFunction("MDURATION", Mduration) RegisterFunction("PDURATION", Pduration) RegisterFunction("_xlfn.PDURATION", Pduration) + RegisterFunction("ACCRINTM", Accrintm) + RegisterFunction("AMORDEGRC", Amordegrc) + RegisterFunction("AMORLINC", Amorlinc) + RegisterFunction("COUPDAYBS", Coupdaybs) + RegisterFunction("COUPDAYS", Coupdays) + RegisterFunction("COUPDAYSNC", Coupdaysnc) + RegisterFunction("COUPNUM", Coupnum) + RegisterFunction("COUPNCD", Coupncd) + RegisterFunction("COUPPCD", Couppcd) + RegisterFunction("CUMIPMT", Cumipmt) + RegisterFunction("CUMPRINC", Cumprinc) } // Duration implements the Excel DURATION function. func Duration(args []Result) Result { - settlementDate, maturityDate, coupon, yield, freq, basis, err := parseDurationData(args, "DURATION") + parsedArgs, err := parseDurationData(args, "DURATION") if err.Type == ResultTypeError { return err } - return getDuration(dateFromDays(settlementDate), dateFromDays(maturityDate), coupon, yield, freq, basis) + 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 { - settlementDate, maturityDate, coupon, yield, freq, basis, err := parseDurationData(args, "MDURATION") + parsedArgs, err := parseDurationData(args, "MDURATION") if err.Type == ResultTypeError { return err } - duration := getDuration(dateFromDays(settlementDate), dateFromDays(maturityDate), coupon, yield, freq, basis) + 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 } @@ -71,8 +96,162 @@ func Pduration(args []Result) Result { return MakeNumberResult((math.Log10(specifiedValue) - math.Log10(currentValue)) / math.Log10(1 + rate)) } -// getCouppcd finds last coupon date before settlement (can be equal to settlement). -func getCouppcd(settlementDate, maturityDate time.Time, freq int) time.Time { +type couponArgs struct { + settlementDate float64 + maturityDate float64 + freq int + basis int +} + +// Coupdaybs implements the Excel COUPDAYBS function. +func Coupdaybs(args []Result) Result { + parsedArgs, err := parseCouponArgs(args, "COUPDAYBS") + if err.Type == ResultTypeError { + return err + } + settlementDate := dateFromDays(parsedArgs.settlementDate) + maturityDate := dateFromDays(parsedArgs.maturityDate) + freq := parsedArgs.freq + basis := parsedArgs.basis + pcd := couppcd(settlementDate, maturityDate, freq, basis) + return MakeNumberResult(getDiff(pcd, settlementDate, basis)) +} + +// Coupdays implements the Excel COUPDAYS function. +func Coupdays(args []Result) Result { + parsedArgs, err := parseCouponArgs(args, "COUPDAYS") + if err.Type == ResultTypeError { + return err + } + settlementDate := dateFromDays(parsedArgs.settlementDate) + maturityDate := dateFromDays(parsedArgs.maturityDate) + freq := parsedArgs.freq + basis := parsedArgs.basis + if basis == 1 { + pcd := couppcd(settlementDate, maturityDate, freq, 1) + next := pcd.AddDate(0, 12 / freq, 0) + return MakeNumberResult(getDiff(pcd, next, basis)) + } + return MakeNumberResult(float64(getDaysInYear(0, basis)) / float64(freq)) +} + +// Coupdaysnc implements the Excel COUPDAYSNC function. +func Coupdaysnc(args []Result) Result { + parsedArgs, err := parseCouponArgs(args, "COUPDAYSNC") + if err.Type == ResultTypeError { + return err + } + settlementDate := dateFromDays(parsedArgs.settlementDate) + maturityDate := dateFromDays(parsedArgs.maturityDate) + freq := parsedArgs.freq + basis := parsedArgs.basis + ncd := coupncd(settlementDate, maturityDate, freq) + return MakeNumberResult(getDiff(settlementDate, ncd, basis)) +} + +// Couppcd implements the Excel COUPPCD function. +func Couppcd(args []Result) Result { + parsedArgs, err := parseCouponArgs(args, "COUPPCD") + if err.Type == ResultTypeError { + return err + } + settlementDate := dateFromDays(parsedArgs.settlementDate) + maturityDate := dateFromDays(parsedArgs.maturityDate) + freq := parsedArgs.freq + basis := parsedArgs.basis + pcd := couppcd(settlementDate, maturityDate, freq, basis) + y, m, d := pcd.Date() + return MakeNumberResult(daysFromDate(y, int(m), d)) +} + +// Coupnum implements the Excel COUPNUM function. +func Coupnum(args []Result) Result { + parsedArgs, err := parseCouponArgs(args, "COUPNUM") + 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) + if err.Type == ResultTypeError { + return err + } + return MakeNumberResult(cn) +} + +// Coupncd implements the Excel COUPNCD function. +func Coupncd(args []Result) Result { + parsedArgs, err := parseCouponArgs(args, "COUPNCD") + if err.Type == ResultTypeError { + return err + } + settlementDate := dateFromDays(parsedArgs.settlementDate) + maturityDate := dateFromDays(parsedArgs.maturityDate) + freq := parsedArgs.freq + ncd := coupncd(settlementDate, maturityDate, freq) + y, m, d := ncd.Date() + return MakeNumberResult(daysFromDate(y, int(m), d)) +} + +func coupncd(settlementDate, maturityDate time.Time, freq int) time.Time { + ncd := time.Date(settlementDate.Year(), maturityDate.Month(), maturityDate.Day(), 0, 0, 0, 0, time.UTC) + if ncd.After(settlementDate) { + ncd = ncd.AddDate(-1, 0, 0) + } + for !ncd.After(settlementDate) { + ncd = ncd.AddDate(0, 12 / freq, 0) + } + return ncd +} + +func parseCouponArgs(args []Result, funcName string) (*couponArgs, Result) { + argsNum := len(args) + 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 := 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") + } + if args[2].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires frequency to be number argument") + } + freq := args[2].ValueNumber + if !checkFreq(freq) { + return nil, MakeErrorResult("Incorrect frequency for " + funcName) + } + basis := 0 + if argsNum == 4 { + if args[3].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires basis to be number argument") + } + basis = int(args[3].ValueNumber) + if !checkBasis(basis) { + return nil, MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for " + funcName) + } + } + return &couponArgs{ + settlementDate, + maturityDate, + int(freq), + basis, + }, MakeEmptyResult() +} + +// couppcd finds last coupon date before settlement (can be equal to settlement). +func couppcd(settlementDate, maturityDate time.Time, freq, basis int) time.Time { rDate := maturityDate diffYears := settlementDate.Year() - maturityDate.Year() rDate = rDate.AddDate(diffYears, 0, 0) @@ -86,41 +265,44 @@ func getCouppcd(settlementDate, maturityDate time.Time, freq int) time.Time { return rDate } -// getCoupnum gets count of coupon dates. -func getCoupnum(settlementDate, maturityDate time.Time, freq, basis int) float64 { +// coupnum gets count of coupon dates. +func coupnum(settlementDate, maturityDate time.Time, freq, basis int) (float64, Result) { if maturityDate.After(settlementDate) { - aDate := getCouppcd(settlementDate, maturityDate, freq) + aDate := couppcd(settlementDate, maturityDate, freq, basis) months := (maturityDate.Year() - aDate.Year()) * 12 + int(maturityDate.Month()) - int(aDate.Month()) - return float64(months * freq) / 12.0 + return float64(months * freq) / 12.0, MakeEmptyResult() } - return 0.0 // replace for error + return 0, MakeErrorResultType(ErrorTypeNum, "Settlement date should be before maturity date") } // 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 time.Time, coup, yield, freq float64, basis int) Result { +func getDuration(settlementDate, maturityDate, coup, yield, freq float64, basis int) Result { fracResult := yearFrac(settlementDate, maturityDate, basis) if fracResult.Type == ResultTypeError { return fracResult } frac := fracResult.ValueNumber - nCoups := getCoupnum(settlementDate, maturityDate, int(freq), basis) + coups, err := coupnum(dateFromDays(settlementDate), dateFromDays(maturityDate), int(freq), basis) + if err.Type == ResultTypeError { + return err + } duration := 0.0 p := 0.0 coup *= 100 / freq yield /= freq yield++ - diff := frac * freq - nCoups - for t := 1.0; t < nCoups; t++ { + diff := frac * freq - coups + for t := 1.0; t < coups; t++ { tDiff := t + diff add := coup / math.Pow(yield, tDiff) p += add duration += tDiff * add } - add := (coup + 100) / math.Pow(yield, nCoups + diff) + add := (coup + 100) / math.Pow(yield, coups + diff) p += add - duration += (nCoups + diff) * add + duration += (coups + diff) * add duration /= p duration /= freq @@ -128,10 +310,19 @@ func getDuration(settlementDate, maturityDate time.Time, coup, yield, freq float return MakeNumberResult(duration) } +type durationArgs struct { + settlementDate float64 + maturityDate float64 + coupon float64 + yield float64 + freq float64 + basis int +} + // 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) (float64, float64, float64, float64, float64, int, Result) { +func parseDurationData(args []Result, funcName string) (*durationArgs, Result) { if len(args) != 5 && len(args) != 6 { - return 0, 0, 0, 0, 0, 0, MakeErrorResult(funcName + " requires five or six arguments") + return nil, MakeErrorResult(funcName + " requires five or six arguments") } var settlementDate, maturityDate float64 settlementResult := args[0] @@ -141,11 +332,11 @@ func parseDurationData(args []Result, funcName string) (float64, float64, float6 case ResultTypeString: settlementFromString := DateValue([]Result{settlementResult}) if settlementFromString.Type == ResultTypeError { - return 0, 0, 0, 0, 0, 0, MakeErrorResult("Incorrect settltment date for " + funcName) + return nil, MakeErrorResult("Incorrect settltment date for " + funcName) } settlementDate = settlementFromString.ValueNumber default: - return 0, 0, 0, 0, 0, 0, MakeErrorResult("Incorrect argument for " + funcName) + return nil, MakeErrorResult("Incorrect argument for " + funcName) } maturityResult := args[1] switch maturityResult.Type { @@ -154,49 +345,447 @@ func parseDurationData(args []Result, funcName string) (float64, float64, float6 case ResultTypeString: maturityFromString := DateValue([]Result{maturityResult}) if maturityFromString.Type == ResultTypeError { - return 0, 0, 0, 0, 0, 0, MakeErrorResult("Incorrect settltment date for " + funcName) + return nil, MakeErrorResult("Incorrect settltment date for " + funcName) } maturityDate = maturityFromString.ValueNumber default: - return 0, 0, 0, 0, 0, 0, MakeErrorResult("Incorrect argument for " + funcName) + return nil, MakeErrorResult("Incorrect argument for " + funcName) } if settlementDate >= maturityDate { - return 0, 0, 0, 0, 0, 0, MakeErrorResultType(ErrorTypeNum, "Settlement date should be before maturity date") + return nil, MakeErrorResultType(ErrorTypeNum, "Settlement date should be before maturity date") } couponResult := args[2] if couponResult.Type != ResultTypeNumber { - return 0, 0, 0, 0, 0, 0, MakeErrorResult(funcName + " requires third argument of type number") + return nil, MakeErrorResult(funcName + " requires third argument of type number") } coupon := couponResult.ValueNumber if coupon < 0 { - return 0, 0, 0, 0, 0, 0, MakeErrorResultType(ErrorTypeNum, "Coupon rate should not be negative") + return nil, MakeErrorResultType(ErrorTypeNum, "Coupon rate should not be negative") } yieldResult := args[3] if yieldResult.Type != ResultTypeNumber { - return 0, 0, 0, 0, 0, 0, MakeErrorResult(funcName + " requires fourth argument of type number") + return nil, MakeErrorResult(funcName + " requires fourth argument of type number") } yield := yieldResult.ValueNumber if yield < 0 { - return 0, 0, 0, 0, 0, 0, MakeErrorResultType(ErrorTypeNum, "Yield rate should not be negative") + return nil, MakeErrorResultType(ErrorTypeNum, "Yield rate should not be negative") } freqResult := args[4] if freqResult.Type != ResultTypeNumber { - return 0, 0, 0, 0, 0, 0, MakeErrorResult(funcName + " requires fifth argument of type number") + return nil, MakeErrorResult(funcName + " requires fifth argument of type number") } freq := float64(int(freqResult.ValueNumber)) - if freq != 1 && freq != 2 && freq != 4 { - return 0, 0, 0, 0, 0, 0, MakeErrorResultType(ErrorTypeNum, "Incorrect frequence value") + if !checkFreq(freq) { + return nil, MakeErrorResultType(ErrorTypeNum, "Incorrect frequence value") } basis := 0 if len(args) == 6 { basisResult := args[5] if basisResult.Type != ResultTypeNumber { - return 0, 0, 0, 0, 0, 0, MakeErrorResult(funcName + " requires sixth argument of type number") + return nil, MakeErrorResult(funcName + " requires sixth argument of type number") } basis = int(basisResult.ValueNumber) - if basis < 0 || basis > 4 { - return 0, 0, 0, 0, 0, 0, MakeErrorResultType(ErrorTypeNum, "Incorrect basis value") + if !checkBasis(basis) { + return nil, MakeErrorResultType(ErrorTypeNum, "Incorrect basis value") } } - return settlementDate, maturityDate, coupon, yield, freq, basis, MakeEmptyResult() + return &durationArgs{ + settlementDate, + maturityDate, + coupon, + yield, + freq, + basis, + }, MakeEmptyResult() +} + +func checkFreq(freq float64) bool { + return freq == 1 || freq == 2 || freq == 4 +} + +func checkBasis(basis int) bool { + return basis >= 0 && basis <= 4 +} + +// Accrintm implements the Excel ACCRINTM function. +func Accrintm(args []Result) Result { + argsNum := len(args) + 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") + } + settlement := args[1].ValueNumber + if issue >= settlement { + return MakeErrorResultType(ErrorTypeNum, "ACCRINTM requires settlement date to be later than issue date") + } + if args[2].Type != ResultTypeNumber { + return MakeErrorResult("ACCRINTM requires rate to be number argument") + } + rate := args[2].ValueNumber + if rate <= 0 { + return MakeErrorResultType(ErrorTypeNum, "ACCRINTM requires rate to be positive number argument") + } + if args[3].Type != ResultTypeNumber { + return MakeErrorResult("ACCRINTM requires par to be number argument") + } + par := args[3].ValueNumber + if par <= 0 { + return MakeErrorResultType(ErrorTypeNum, "ACCRINTM requires par to be positive number argument") + } + basis := 0 + if argsNum == 5 { + if args[4].Type != ResultTypeNumber { + return MakeErrorResult("ACCRINTM requires basis to be number argument") + } + basis = int(args[4].ValueNumber) + if !checkBasis(basis) { + return MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for ACCRINTM") + } + } + fracResult := yearFrac(issue, settlement, basis) + if fracResult.Type == ResultTypeError { + return fracResult + } + return MakeNumberResult(par * rate * fracResult.ValueNumber) +} + +// Amordegrc implements the Excel AMORDEGRC function. +func Amordegrc(args []Result) Result { + parsedArgs, err := parseAmorArgs(args, "AMORDEGRC") + if err.Type == ResultTypeError { + return err + } + cost := parsedArgs.cost + datePurchased := parsedArgs.datePurchased + firstPeriod := parsedArgs.firstPeriod + salvage := parsedArgs.salvage + period := parsedArgs.period + rate := parsedArgs.rate + if rate >= 0.5 { + return MakeErrorResultType(ErrorTypeNum, "AMORDEGRC requires rate to be less than 0.5") + } + basis := parsedArgs.basis + + lifeOfAssets := 1.0 / rate + amorCoeff := 2.5 + if lifeOfAssets < 3 { + amorCoeff = 1 + } else if lifeOfAssets < 5 { + amorCoeff = 1.5 + } else if lifeOfAssets <= 6 { + amorCoeff = 2 + } + + rate *= amorCoeff + yfResult := yearFrac(datePurchased, firstPeriod, basis) + if yfResult.Type == ResultTypeError { + return MakeErrorResult("incorrect dates for AMORDEGRC") + } + nRate := mathRound(yfResult.ValueNumber * rate * cost) + cost -= nRate + rest := cost - salvage + + for n := 0; n < period; n++ { + nRate = mathRound(rate * cost) + rest -= nRate + if rest < 0 { + switch period - n { + case 0: + case 1: + return MakeNumberResult(mathRound(cost * 0.5)) + default: + return MakeNumberResult(0) + } + } + cost -= nRate + } + + return MakeNumberResult(nRate) +} + +// Amorlinc implements the Excel AMORLINC function. +func Amorlinc(args []Result) Result { + parsedArgs, err := parseAmorArgs(args, "AMORLINC") + if err.Type == ResultTypeError { + return err + } + cost := parsedArgs.cost + datePurchased := parsedArgs.datePurchased + firstPeriod := parsedArgs.firstPeriod + salvage := parsedArgs.salvage + period := parsedArgs.period + rate := parsedArgs.rate + basis := parsedArgs.basis + + yfResult := yearFrac(datePurchased, firstPeriod, basis) + if yfResult.Type == ResultTypeError { + return MakeErrorResult("incorrect dates for AMORLINC") + } + r0 := yfResult.ValueNumber * rate * cost + if period == 0 { + return MakeNumberResult(r0) + } + + oneRate := cost * rate + costDelta := cost - salvage + numOfFullPeriods := int((costDelta - r0) / oneRate) + + if period <= numOfFullPeriods { + return MakeNumberResult(oneRate) + } else if period == numOfFullPeriods + 1 { + return MakeNumberResult(costDelta - oneRate * float64(numOfFullPeriods) - r0) + } else { + return MakeNumberResult(0) + } +} + +type amorArgs struct { + cost float64 + datePurchased float64 + firstPeriod float64 + salvage float64 + period int + rate float64 + basis int +} + +func parseAmorArgs(args []Result, funcName string) (*amorArgs, Result) { + argsNum := len(args) + if argsNum != 6 && argsNum != 7 { + return nil, MakeErrorResult(funcName + " requires six or seven arguments") + } + if args[0].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires cost to be number argument") + } + cost := args[0].ValueNumber + if cost < 0 { + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires cost to be positive") + } + 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") + } + if args[2].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires first period to be number argument") + } + firstPeriod := args[2].ValueNumber + if firstPeriod < datePurchased { + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires first period to be later than date purchased") + } + if args[3].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires salvage to be number argument") + } + salvage := args[3].ValueNumber + if salvage < 0 || salvage > cost { + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires salvage to be between 0 and the initial cost") + } + if args[4].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires period to be number argument") + } + period := int(args[4].ValueNumber) + if period < 0 { + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires period to be non-negative") + } + if args[5].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires depreciation rate to be number argument") + } + rate := args[5].ValueNumber + if rate < 0 { + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires rate to be more than 0 and less than 0.5") + } + basis := 0 + if argsNum == 7 { + if args[6].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires basis to be number argument") + } + basis = int(args[6].ValueNumber) + if !checkBasis(basis) || basis == 2 { + return nil, MakeErrorResultType(ErrorTypeNum, "Incorrect basis argument for " + funcName) + } + } + return &amorArgs{ + cost, + datePurchased, + firstPeriod, + salvage, + period, + rate, + basis, + }, MakeEmptyResult() +} + +func mathRound(x float64) float64 { + return float64(int(x + 0.5)) +} + +type cumulArgs struct { + rate float64 + nPer float64 + pv float64 + startPeriod float64 + endPeriod float64 + t int +} + +// Cumipmt implements the Excel CUMIPMT function. +func Cumipmt(args []Result) Result { + parsedArgs, err := parseCumulArgs(args, "CUMIPMT") + if err.Type == ResultTypeError { + return err + } + rate := parsedArgs.rate + nPer := parsedArgs.nPer + pv := parsedArgs.pv + startPeriod := parsedArgs.startPeriod + endPeriod := parsedArgs.endPeriod + t := parsedArgs.t + + payment := pmt(rate, nPer, pv, 0, t) + interest := 0.0 + if startPeriod == 1 { + if t == 0 { + interest = -pv + startPeriod++ + } + } + for i := startPeriod; i <= endPeriod; i++ { + if t == 1 { + interest += fv(rate, i - 2, payment, pv, 1) - payment + } else { + interest += fv(rate, i - 1, payment, pv, 0) + } + } + interest *= rate + return MakeNumberResult(interest) +} + +// Cumprinc implements the Excel CUMPRINC function. +func Cumprinc(args []Result) Result { + parsedArgs, err := parseCumulArgs(args, "CUMPRINC") + if err.Type == ResultTypeError { + return err + } + rate := parsedArgs.rate + nPer := parsedArgs.nPer + pv := parsedArgs.pv + startPeriod := parsedArgs.startPeriod + endPeriod := parsedArgs.endPeriod + t := parsedArgs.t + + payment := pmt(rate, nPer, pv, 0, t) + principal := 0.0 + if startPeriod == 1 { + if t == 0 { + principal = payment + pv * rate + } else { + principal = payment + } + startPeriod++ + } + for i := startPeriod; i <= endPeriod; i++ { + if t == 1 { + principal += payment - (fv(rate, i - 2, payment, pv, 1) - payment) * rate + } else { + principal += payment - fv(rate, i - 1, payment, pv, 0) * rate + } + } + return MakeNumberResult(principal) +} + +func parseCumulArgs(args []Result, funcName string) (*cumulArgs, Result) { + if len(args) != 6 { + return nil, MakeErrorResult(funcName + " requires six arguments") + } + if args[0].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires rate to be number argument") + } + rate := args[0].ValueNumber + if rate <= 0 { + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires rate to be positive number argument") + } + if args[1].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires number of periods to be number argument") + } + nPer := args[1].ValueNumber + if nPer <= 0 { + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires number of periods to be positive number argument") + } + if args[2].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires present value to be number argument") + } + pv := args[2].ValueNumber + if pv <= 0 { + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires present value to be positive number argument") + } + if args[3].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires start period to be number argument") + } + startPeriod := args[3].ValueNumber + if startPeriod <= 0 { + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires start period to be positive number argument") + } + if args[4].Type != ResultTypeNumber { + return nil, MakeErrorResult(funcName + " requires end period to be number argument") + } + endPeriod := args[4].ValueNumber + if endPeriod <= 0 { + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires end period to be positive number argument") + } + if endPeriod < startPeriod { + 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") + } + t := int(args[5].ValueNumber) + if t != 0 && t != 1 { + return nil, MakeErrorResultType(ErrorTypeNum, funcName + " requires type to be 0 or 1") + } + return &cumulArgs{ + rate, + nPer, + pv, + startPeriod, + endPeriod, + t, + }, MakeEmptyResult() +} + +func pmt(rate, periods, present, future float64, t int ) float64 { + var result float64 + if rate == 0 { + result = (present + future) / periods + } else { + term := math.Pow(1 + rate, periods) + if t == 1 { + result = (future * rate / (term - 1) + present * rate / (1 - 1 / term)) / (1 + rate) + } else { + result = future * rate / (term - 1) + present * rate / (1 - 1 / term) + } + } + return -result +} + +func fv(rate, periods, payment, value float64, t int) float64 { + var result float64 + if rate == 0 { + result = value + payment * periods + } else { + term := math.Pow(1 + rate, periods) + if t == 1 { + result = value * term + payment * (1 + rate) * (term - 1) / rate + } else { + result = value * term + payment * (term - 1) / rate + } + } + return -result } diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index a3860540..15ed9820 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -1659,3 +1659,212 @@ func TestIf(t *testing.T) { runTests(t, ctx, td) } + +func TestAccrintm(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + 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`}, + } + + runTests(t, ctx, td) +} + +func TestAmordegrc(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=AMORDEGRC(2400,39679,39813,300,1,0.15,1)`, `776 ResultTypeNumber`}, + {`=AMORDEGRC(2400,39679,39813,300,1,0.15,2)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestAmorlinc(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=AMORLINC(2400,39679,39813,300,1,0.15,1)`, `360 ResultTypeNumber`}, + {`=AMORLINC(2400,39679,39813,300,1,0.15,2)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestCoupdaybs(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=COUPDAYBS(40568,40862,2,0)`, `70 ResultTypeNumber`}, + {`=COUPDAYBS(40568,40862,2,1)`, `71 ResultTypeNumber`}, + {`=COUPDAYBS(40568,40862,2,2)`, `71 ResultTypeNumber`}, + {`=COUPDAYBS(40568,40862,2,3)`, `71 ResultTypeNumber`}, + {`=COUPDAYBS(40568,40862,2,4)`, `70 ResultTypeNumber`}, + {`=COUPDAYBS(40599,40862,2,0)`, `100 ResultTypeNumber`}, + {`=COUPDAYBS(40599,40862,2,1)`, `102 ResultTypeNumber`}, + {`=COUPDAYBS(40599,40862,2,2)`, `102 ResultTypeNumber`}, + {`=COUPDAYBS(40599,40862,2,3)`, `102 ResultTypeNumber`}, + {`=COUPDAYBS(40599,40862,2,4)`, `100 ResultTypeNumber`}, + {`=COUPDAYBS(40811,40862,2,0)`, `130 ResultTypeNumber`}, + {`=COUPDAYBS(40811,40862,2,1)`, `133 ResultTypeNumber`}, + {`=COUPDAYBS(40811,40862,2,2)`, `133 ResultTypeNumber`}, + {`=COUPDAYBS(40811,40862,2,3)`, `133 ResultTypeNumber`}, + {`=COUPDAYBS(40811,40862,2,4)`, `130 ResultTypeNumber`}, + {`=COUPDAYBS(40872,40568,2,1)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestCoupdays(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=COUPDAYS(40964,41228,1,0)`, `360 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,1,1)`, `366 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,1,2)`, `360 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,1,3)`, `365 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,1,4)`, `360 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,2,0)`, `180 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,2,1)`, `182 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,2,2)`, `180 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,2,3)`, `182.5 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,2,4)`, `180 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,4,0)`, `90 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,4,1)`, `90 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,4,2)`, `90 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,4,3)`, `91.25 ResultTypeNumber`}, + {`=COUPDAYS(40964,41228,4,4)`, `90 ResultTypeNumber`}, + {`=COUPDAYS(41228,40964,2,1)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestCoupdaysnc(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=COUPDAYSNC(40933,41228,1,0)`, `290 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,1,1)`, `295 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,1,2)`, `295 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,1,3)`, `295 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,1,4)`, `290 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,2,0)`, `110 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,2,1)`, `111 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,2,2)`, `111 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,2,3)`, `111 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,2,4)`, `110 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,4,0)`, `20 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,4,1)`, `21 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,4,2)`, `21 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,4,3)`, `21 ResultTypeNumber`}, + {`=COUPDAYSNC(40933,41228,4,4)`, `20 ResultTypeNumber`}, + {`=COUPDAYSNC(41228,40933,2,1)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestCoupncd(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=COUPNCD(40568,40862,1,1)`, `40862 ResultTypeNumber`}, + {`=COUPNCD(40568,40862,2,1)`, `40678 ResultTypeNumber`}, + {`=COUPNCD(40568,40862,4,1)`, `40589 ResultTypeNumber`}, + {`=COUPNCD(40872,40568,2,1)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestCouppcd(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=COUPPCD(40568,40862,2,1)`, `40497 ResultTypeNumber`}, + {`=COUPPCD(40872,40568,2,1)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestCoupnum(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=COUPNUM(39107,39767,2,1)`, `4 ResultTypeNumber`}, + {`=COUPNUM(39767,39107,2,1)`, `#NUM! ResultTypeError`}, + } + + runTests(t, ctx, td) +} + +func TestCumipmt(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(0.09) + sheet.Cell("A2").SetNumber(30) + sheet.Cell("A3").SetNumber(125000) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=CUMIPMT(A1/12,A2*12,A3,13,24,0)`, `-11135.23213 ResultTypeNumber`}, + {`=CUMIPMT(A1/12,A2*12,A3,1,1,0)`, `-937.5 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +} + +func TestCumprinc(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetNumber(0.09) + sheet.Cell("A2").SetNumber(30) + sheet.Cell("A3").SetNumber(125000) + + ctx := sheet.FormulaContext() + + td := []testStruct{ + {`=CUMPRINC(A1/12,A2*12,A3,13,24,0)`, `-934.10712342 ResultTypeNumber`}, + {`=CUMPRINC(A1/12,A2*12,A3,1,1,0)`, `-68.27827118 ResultTypeNumber`}, + } + + runTests(t, ctx, td) +}