Financial functions - part 1 (#359)

* ACCRINTM, COUPNUM, COUPPCD, AMORDEGRC, AMORLINC
* COUPDAYBS
* COUPNCD, COUPDAYS, COUPDAYSNC
* CUMIPMT, CUMPRINC
This commit is contained in:
Vyacheslav Zgordan 2019-12-14 18:08:04 +03:00 committed by Gunnsteinn Hall
parent e17f3d3164
commit 64edd6c139
3 changed files with 961 additions and 39 deletions

View File

@ -416,10 +416,14 @@ func validateDate(year, month, day int) bool {
if day < 1 { if day < 1 {
return false return false
} }
return day <= getDaysInMonth(year, month)
}
func getDaysInMonth(year, month int) int {
if month == 2 && isLeapYear(year) { if month == 2 && isLeapYear(year) {
return day <= 29 return 29
} else { } 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 { if err != nil {
return MakeErrorResult("incorrect end date") 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() startDateS := startDate.Unix()
endDateS := endDate.Unix() endDateS := endDate.Unix()
sy, sm, sd := startDate.Date() sy, sm, sd := startDate.Date()
@ -821,6 +829,21 @@ func yearFrac(startDate, endDate time.Time, basis int) Result {
return MakeErrorResultType(ErrorTypeValue, "") 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 { func makeDateS(y int, m time.Month, d int) int64 {
if y == 1900 && int(m) <= 2 { if y == 1900 && int(m) <= 2 {
d-- d--
@ -855,3 +878,104 @@ func feb29Between(date1, date2 time.Time) bool {
var mar1year2 = makeDateS(year2, time.March, 1) var mar1year2 = makeDateS(year2, time.March, 1)
return (isLeapYear(year2) && date2S >= mar1year2 && date1S < mar1year2) 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
}

View File

@ -17,24 +17,49 @@ func init() {
RegisterFunction("MDURATION", Mduration) RegisterFunction("MDURATION", Mduration)
RegisterFunction("PDURATION", Pduration) RegisterFunction("PDURATION", Pduration)
RegisterFunction("_xlfn.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. // Duration implements the Excel DURATION function.
func Duration(args []Result) Result { func Duration(args []Result) Result {
settlementDate, maturityDate, coupon, yield, freq, basis, err := parseDurationData(args, "DURATION") parsedArgs, err := parseDurationData(args, "DURATION")
if err.Type == ResultTypeError { if err.Type == ResultTypeError {
return err 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. // Mduration implements the Excel MDURATION function.
func Mduration(args []Result) Result { func Mduration(args []Result) Result {
settlementDate, maturityDate, coupon, yield, freq, basis, err := parseDurationData(args, "MDURATION") parsedArgs, err := parseDurationData(args, "MDURATION")
if err.Type == ResultTypeError { if err.Type == ResultTypeError {
return err 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 { if duration.Type == ResultTypeError {
return duration return duration
} }
@ -71,8 +96,162 @@ func Pduration(args []Result) Result {
return MakeNumberResult((math.Log10(specifiedValue) - math.Log10(currentValue)) / math.Log10(1 + rate)) return MakeNumberResult((math.Log10(specifiedValue) - math.Log10(currentValue)) / math.Log10(1 + rate))
} }
// getCouppcd finds last coupon date before settlement (can be equal to settlement). type couponArgs struct {
func getCouppcd(settlementDate, maturityDate time.Time, freq int) time.Time { 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 rDate := maturityDate
diffYears := settlementDate.Year() - maturityDate.Year() diffYears := settlementDate.Year() - maturityDate.Year()
rDate = rDate.AddDate(diffYears, 0, 0) rDate = rDate.AddDate(diffYears, 0, 0)
@ -86,41 +265,44 @@ func getCouppcd(settlementDate, maturityDate time.Time, freq int) time.Time {
return rDate return rDate
} }
// getCoupnum gets count of coupon dates. // coupnum gets count of coupon dates.
func getCoupnum(settlementDate, maturityDate time.Time, freq, basis int) float64 { func coupnum(settlementDate, maturityDate time.Time, freq, basis int) (float64, Result) {
if maturityDate.After(settlementDate) { 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()) 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. // 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) fracResult := yearFrac(settlementDate, maturityDate, basis)
if fracResult.Type == ResultTypeError { if fracResult.Type == ResultTypeError {
return fracResult return fracResult
} }
frac := fracResult.ValueNumber 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 duration := 0.0
p := 0.0 p := 0.0
coup *= 100 / freq coup *= 100 / freq
yield /= freq yield /= freq
yield++ yield++
diff := frac * freq - nCoups diff := frac * freq - coups
for t := 1.0; t < nCoups; t++ { for t := 1.0; t < coups; t++ {
tDiff := t + diff tDiff := t + diff
add := coup / math.Pow(yield, tDiff) add := coup / math.Pow(yield, tDiff)
p += add p += add
duration += tDiff * add duration += tDiff * add
} }
add := (coup + 100) / math.Pow(yield, nCoups + diff) add := (coup + 100) / math.Pow(yield, coups + diff)
p += add p += add
duration += (nCoups + diff) * add duration += (coups + diff) * add
duration /= p duration /= p
duration /= freq duration /= freq
@ -128,10 +310,19 @@ func getDuration(settlementDate, maturityDate time.Time, coup, yield, freq float
return MakeNumberResult(duration) 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 // 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 { 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 var settlementDate, maturityDate float64
settlementResult := args[0] settlementResult := args[0]
@ -141,11 +332,11 @@ func parseDurationData(args []Result, funcName string) (float64, float64, float6
case ResultTypeString: case ResultTypeString:
settlementFromString := DateValue([]Result{settlementResult}) settlementFromString := DateValue([]Result{settlementResult})
if settlementFromString.Type == ResultTypeError { 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 settlementDate = settlementFromString.ValueNumber
default: default:
return 0, 0, 0, 0, 0, 0, MakeErrorResult("Incorrect argument for " + funcName) return nil, MakeErrorResult("Incorrect argument for " + funcName)
} }
maturityResult := args[1] maturityResult := args[1]
switch maturityResult.Type { switch maturityResult.Type {
@ -154,49 +345,447 @@ func parseDurationData(args []Result, funcName string) (float64, float64, float6
case ResultTypeString: case ResultTypeString:
maturityFromString := DateValue([]Result{maturityResult}) maturityFromString := DateValue([]Result{maturityResult})
if maturityFromString.Type == ResultTypeError { 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 maturityDate = maturityFromString.ValueNumber
default: default:
return 0, 0, 0, 0, 0, 0, MakeErrorResult("Incorrect argument for " + funcName) return nil, MakeErrorResult("Incorrect argument for " + funcName)
} }
if settlementDate >= maturityDate { 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] couponResult := args[2]
if couponResult.Type != ResultTypeNumber { 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 coupon := couponResult.ValueNumber
if coupon < 0 { 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] yieldResult := args[3]
if yieldResult.Type != ResultTypeNumber { 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 yield := yieldResult.ValueNumber
if yield < 0 { 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] freqResult := args[4]
if freqResult.Type != ResultTypeNumber { 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)) freq := float64(int(freqResult.ValueNumber))
if freq != 1 && freq != 2 && freq != 4 { if !checkFreq(freq) {
return 0, 0, 0, 0, 0, 0, MakeErrorResultType(ErrorTypeNum, "Incorrect frequence value") return nil, MakeErrorResultType(ErrorTypeNum, "Incorrect frequence value")
} }
basis := 0 basis := 0
if len(args) == 6 { if len(args) == 6 {
basisResult := args[5] basisResult := args[5]
if basisResult.Type != ResultTypeNumber { 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) basis = int(basisResult.ValueNumber)
if basis < 0 || basis > 4 { if !checkBasis(basis) {
return 0, 0, 0, 0, 0, 0, MakeErrorResultType(ErrorTypeNum, "Incorrect basis value") 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
} }

View File

@ -1659,3 +1659,212 @@ func TestIf(t *testing.T) {
runTests(t, ctx, td) 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)
}