Excel functions part 4 (#353)

* DURATION, MDURATION
* PDURATION
* ROW, ROWS
* tests for LOOKUP and VLOOKUP
* LARGE, SMALL
* LOWER
* REPLACE
* TEXTJOIN
* INDEX
This commit is contained in:
Vyacheslav Zgordan 2019-12-05 16:03:20 +03:00 committed by Gunnsteinn Hall
parent 055ebf0f4c
commit 8fadaaeecf
10 changed files with 895 additions and 49 deletions

View File

@ -0,0 +1,45 @@
// Package mergesort provides an implementation for merge sort sorting algorithm
package mergesort
// MergeSort receives a slice and returns a new sorted slice
func MergeSort(array []float64) []float64 {
if len(array) <= 1 {
result := make([]float64, len(array))
copy(result, array)
return result
}
mid := len(array) / 2
leftArray := MergeSort(array[:mid])
rightArray := MergeSort(array[mid:])
result := make([]float64, len(array))
arrayIndex := 0
leftIndex := 0
rightIndex := 0
for leftIndex < len(leftArray) && rightIndex < len(rightArray) {
if leftArray[leftIndex] <= rightArray[rightIndex] {
result[arrayIndex] = leftArray[leftIndex]
leftIndex++
} else {
result[arrayIndex] = rightArray[rightIndex]
rightIndex++
}
arrayIndex++
}
for leftIndex < len(leftArray) {
result[arrayIndex] = leftArray[leftIndex]
leftIndex++
arrayIndex++
}
for rightIndex < len(rightArray) {
result[arrayIndex] = rightArray[rightIndex]
rightIndex++
arrayIndex++
}
return result
}

View File

@ -177,5 +177,4 @@ func TestArrayFormula(t *testing.T) {
if got := sheet.Cell("F2").GetFormattedValue(); got != "8" {
t.Errorf("expected 8 in F2, got %s", got)
}
}

View File

@ -147,26 +147,32 @@ func Days(args []Result) Result {
return MakeErrorResult("DAYS requires two arguments")
}
var sd, ed float64
if args[0].Type == ResultTypeNumber {
switch args[0].Type {
case ResultTypeNumber:
ed = args[0].ValueNumber
} else {
case ResultTypeString:
edResult := DateValue([]Result{args[0]})
if edResult.Type == ResultTypeError {
return MakeErrorResult("Incorrect end date for DAYS")
}
ed = edResult.ValueNumber
default:
return MakeErrorResult("Incorrect argument for DAYS")
}
if args[1].Type == ResultTypeNumber {
switch args[1].Type {
case ResultTypeNumber:
sd = args[1].ValueNumber
if sd < 62 && ed >= 62 {
sd--
}
} else {
case ResultTypeString:
sdResult := DateValue([]Result{args[1]})
if sdResult.Type == ResultTypeError {
return MakeErrorResult("Incorrect start date for DAYS")
}
sd = sdResult.ValueNumber
default:
return MakeErrorResult("Incorrect argument for DAYS")
}
days := float64(int(ed - sd))
return MakeNumberResult(days)
@ -769,11 +775,15 @@ func YearFrac(ctx Context, ev Evaluator, args []Result) Result {
if err != nil {
return MakeErrorResult("incorrect start date")
}
startDateS := startDate.Unix()
endDate, err := getValueAsTime(args[1].Value(), epoch)
if err != nil {
return MakeErrorResult("incorrect end date")
}
return yearFrac(startDate, endDate, basis)
}
func yearFrac(startDate, endDate time.Time, basis int) Result {
startDateS := startDate.Unix()
endDateS := endDate.Unix()
sy, sm, sd := startDate.Date()
ey, em, ed := endDate.Date()

View File

@ -0,0 +1,202 @@
// Copyright 2017 FoxyUtils ehf. All rights reserved.
//
// Use of this source code is governed by the terms of the Affero GNU General
// Public License version 3.0 as published by the Free Software Foundation and
// appearing in the file LICENSE included in the packaging of this file. A
// commercial license can be purchased on https://unidoc.io.
package formula
import (
"time"
"math"
)
func init() {
RegisterFunction("DURATION", Duration)
RegisterFunction("MDURATION", Mduration)
RegisterFunction("PDURATION", Pduration)
RegisterFunction("_xlfn.PDURATION", Pduration)
}
// Duration implements the Excel DURATION function.
func Duration(args []Result) Result {
settlementDate, maturityDate, coupon, yield, freq, basis, err := parseDurationData(args, "DURATION")
if err.Type == ResultTypeError {
return err
}
return getDuration(dateFromDays(settlementDate), dateFromDays(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")
if err.Type == ResultTypeError {
return err
}
duration := getDuration(dateFromDays(settlementDate), dateFromDays(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))
}
// getCouppcd finds last coupon date before settlement (can be equal to settlement).
func getCouppcd(settlementDate, maturityDate time.Time, freq int) time.Time {
rDate := maturityDate
diffYears := settlementDate.Year() - maturityDate.Year()
rDate = rDate.AddDate(diffYears, 0, 0)
if settlementDate.After(rDate) {
rDate = rDate.AddDate(1, 0, 0)
}
monthsToAdd := -12 / freq
for rDate.After(settlementDate) {
rDate = rDate.AddDate(0, monthsToAdd, 0)
}
return rDate
}
// getCoupnum gets count of coupon dates.
func getCoupnum(settlementDate, maturityDate time.Time, freq, basis int) float64 {
if maturityDate.After(settlementDate) {
aDate := getCouppcd(settlementDate, maturityDate, freq)
months := (maturityDate.Year() - aDate.Year()) * 12 + int(maturityDate.Month()) - int(aDate.Month())
return float64(months * freq) / 12.0
}
return 0.0 // replace for error
}
// 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 {
fracResult := yearFrac(settlementDate, maturityDate, basis)
if fracResult.Type == ResultTypeError {
return fracResult
}
frac := fracResult.ValueNumber
nCoups := getCoupnum(settlementDate, maturityDate, int(freq), basis)
duration := 0.0
p := 0.0
coup *= 100 / freq
yield /= freq
yield++
diff := frac * freq - nCoups
for t := 1.0; t < nCoups; t++ {
tDiff := t + diff
add := coup / math.Pow(yield, tDiff)
p += add
duration += tDiff * add
}
add := (coup + 100) / math.Pow(yield, nCoups + diff)
p += add
duration += (nCoups + diff) * add
duration /= p
duration /= freq
return MakeNumberResult(duration)
}
// 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) {
if len(args) != 5 && len(args) != 6 {
return 0, 0, 0, 0, 0, 0, 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 0, 0, 0, 0, 0, 0, MakeErrorResult("Incorrect settltment date for " + funcName)
}
settlementDate = settlementFromString.ValueNumber
default:
return 0, 0, 0, 0, 0, 0, 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 0, 0, 0, 0, 0, 0, MakeErrorResult("Incorrect settltment date for " + funcName)
}
maturityDate = maturityFromString.ValueNumber
default:
return 0, 0, 0, 0, 0, 0, MakeErrorResult("Incorrect argument for " + funcName)
}
if settlementDate >= maturityDate {
return 0, 0, 0, 0, 0, 0, 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")
}
coupon := couponResult.ValueNumber
if coupon < 0 {
return 0, 0, 0, 0, 0, 0, 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")
}
yield := yieldResult.ValueNumber
if yield < 0 {
return 0, 0, 0, 0, 0, 0, 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")
}
freq := float64(int(freqResult.ValueNumber))
if freq != 1 && freq != 2 && freq != 4 {
return 0, 0, 0, 0, 0, 0, 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")
}
basis = int(basisResult.ValueNumber)
if basis < 0 || basis > 4 {
return 0, 0, 0, 0, 0, 0, MakeErrorResultType(ErrorTypeNum, "Incorrect basis value")
}
}
return settlementDate, maturityDate, coupon, yield, freq, basis, MakeEmptyResult()
}

View File

@ -13,6 +13,7 @@ import (
"github.com/unidoc/unioffice/internal/wildcard"
"github.com/unidoc/unioffice/spreadsheet/reference"
"github.com/unidoc/unioffice/internal/mergesort"
)
func init() {
@ -24,7 +25,11 @@ func init() {
RegisterFunctionComplex("OFFSET", Offset)
RegisterFunction("MATCH", Match)
RegisterFunction("HLOOKUP", HLookup)
RegisterFunction("LARGE", Large)
RegisterFunction("LOOKUP", Lookup)
RegisterFunction("ROW", Row)
RegisterFunction("ROWS", Rows)
RegisterFunction("SMALL", Small)
RegisterFunction("VLOOKUP", VLookup)
RegisterFunction("TRANSPOSE", Transpose)
}
@ -79,8 +84,9 @@ func Columns(args []Result) Result {
// Index implements the Excel INDEX function.
func Index(args []Result) Result {
if len(args) < 3 {
return MakeErrorResult("INDEX requires three arguments")
argsNum := len(args)
if argsNum < 2 || argsNum > 3 {
return MakeErrorResult("INDEX requires from one to three arguments")
}
arr := args[0]
if arr.Type != ResultTypeArray && arr.Type != ResultTypeList {
@ -90,29 +96,73 @@ func Index(args []Result) Result {
if rowArg.Type != ResultTypeNumber {
return MakeErrorResult("INDEX requires numeric row argument")
}
colArg := args[2].AsNumber()
if colArg.Type != ResultTypeNumber {
return MakeErrorResult("INDEX requires numeric col argument")
}
row := int(rowArg.ValueNumber) - 1
col := int(colArg.ValueNumber) - 1
col := -1
if argsNum == 3 {
colArg := args[2].AsNumber()
if colArg.Type != ResultTypeNumber {
return MakeErrorResult("INDEX requires numeric col argument")
}
col = int(colArg.ValueNumber) - 1
}
if row == -1 && col == -1 {
return MakeErrorResult("INDEX requires row or col argument")
}
var rowVal []Result
if arr.Type == ResultTypeArray {
if row < 0 || row >= len(arr.ValueArray) {
valueArray := arr.ValueArray
if row < -1 || row >= len(valueArray) {
return MakeErrorResult("INDEX has row out of range")
}
rowVal = arr.ValueArray[row]
if row == -1 {
if col >= len(valueArray[0]) {
return MakeErrorResult("INDEX has col out of range")
}
oneColumnArray := [][]Result{}
for _, row := range valueArray {
v := row[col]
if v.Type == ResultTypeEmpty {
v = MakeNumberResult(0)
}
oneColumnArray = append(oneColumnArray, []Result{v})
}
return MakeArrayResult(oneColumnArray)
}
rowVal = valueArray[row]
} else {
if row < 0 || row >= 1 {
valueList := arr.ValueList
if row < -1 || row >= 1 {
return MakeErrorResult("INDEX has row out of range")
}
rowVal = arr.ValueList
if row == -1 {
if col >= len(valueList) {
return MakeErrorResult("INDEX has col out of range")
}
v := valueList[col]
if v.Type == ResultTypeEmpty {
v = MakeNumberResult(0)
}
return v
}
rowVal = valueList
}
if col < 0 || col > len(rowVal) {
if col < -1 || col > len(rowVal) {
return MakeErrorResult("INDEX has col out of range")
}
if col == -1 {
listResult := []Result{}
for _, v := range rowVal {
if v.Type == ResultTypeEmpty {
listResult = append(listResult, MakeNumberResult(0))
} else {
listResult = append(listResult, v)
}
}
return MakeArrayResult([][]Result{listResult})
}
rv := rowVal[col]
// empty cell returns a zero
if rv.Type == ResultTypeEmpty {
@ -601,3 +651,98 @@ func Transpose(args []Result) Result {
}
return MakeArrayResult(res)
}
// Row implements the Excel ROW function.
func Row(args []Result) Result {
if len(args) < 1 {
return MakeErrorResult("ROW requires one argument")
}
ref := args[0].Ref
if ref.Type != ReferenceTypeCell {
return MakeErrorResult("ROW requires an argument to be of type reference")
}
cr, err := reference.ParseCellReference(ref.Value)
if err != nil {
return MakeErrorResult("Incorrect reference: " + ref.Value)
}
return MakeNumberResult(float64(cr.RowIdx))
}
// Rows implements the Excel ROWS function.
func Rows(args []Result) Result {
if len(args) < 1 {
return MakeErrorResult("ROWS requires one argument")
}
arrResult := args[0]
if arrResult.Type != ResultTypeArray && arrResult.Type != ResultTypeList {
return MakeErrorResult("ROWS requires first argument of type array")
}
arr := arrResult.ValueArray
if len(arr) == 0 {
return MakeErrorResult("ROWS requires array to contain at least 1 row")
}
return MakeNumberResult(float64(len(arr)))
}
// Large implements the Excel LARGE function.
func Large(args []Result) Result {
return kth(args, true)
}
// Small implements the Excel SMALL function.
func Small(args []Result) Result {
return kth(args, false)
}
func kth(args []Result, large bool) Result {
var funcName string
if large {
funcName = "LARGE"
} else {
funcName = "SMALL"
}
if len(args) != 2 {
return MakeErrorResult(funcName + " requires two arguments")
}
arrResult := args[0]
var arr [][]Result
switch arrResult.Type {
case ResultTypeArray:
arr = arrResult.ValueArray
case ResultTypeList:
arr = [][]Result{arrResult.ValueList}
default:
return MakeErrorResult(funcName + " requires first argument of type array")
}
if len(arr) == 0 {
return MakeErrorResult(funcName + " requires array to contain at least 1 row")
}
if args[1].Type != ResultTypeNumber {
return MakeErrorResult(funcName + " requires second argument of type number")
}
kfloat := args[1].ValueNumber
if kfloat < 1 {
return MakeErrorResultType(ErrorTypeNum, funcName + " requires second argument of type number more than 0")
}
k := int(kfloat)
if float64(k) != kfloat {
return MakeErrorResultType(ErrorTypeNum, funcName + " requires second argument of type number more than 0")
}
unsorted := []float64{}
for _, row := range arr {
for _, v := range row {
if v.Type == ResultTypeNumber {
unsorted = append(unsorted, v.ValueNumber)
}
}
}
if k > len(unsorted) {
return MakeErrorResultType(ErrorTypeNum, funcName + " requires second argument of type number less or equal than the number of numbers in the array")
}
sorted := mergesort.MergeSort(unsorted)
if large {
return MakeNumberResult(sorted[len(sorted) - k])
} else {
return MakeNumberResult(sorted[k - 1])
}
}

View File

@ -17,8 +17,7 @@ func init() {
RegisterFunction("_xlfn.IFS", Ifs)
RegisterFunction("NOT", Not)
RegisterFunction("OR", Or)
// TODO: RegisterFunction("SWITCH", Switch) // Only in Excel 2016+
RegisterFunction("TRUE", True) // yup, TRUE()/FALSE() are functions in Excel, news to me...
RegisterFunction("TRUE", True)
RegisterFunction("_xlfn.XOR", Xor)
}

View File

@ -26,7 +26,6 @@ func init() {
RegisterFunction("ACOSH", makeMathWrapper("ASIN", math.Acosh))
RegisterFunction("_xlfn.ACOT", makeMathWrapper("ACOT", func(v float64) float64 { return math.Pi/2 - math.Atan(v) }))
RegisterFunction("_xlfn.ACOTH", makeMathWrapper("ACOTH", func(v float64) float64 { return math.Atanh(1 / v) }))
// TODO: RegisterFunction("_xlfn.AGGREGATE", Aggregate) // lots of dependencies
RegisterFunction("_xlfn.ARABIC", Arabic)
RegisterFunction("ASIN", makeMathWrapper("ASIN", math.Asin))
RegisterFunction("ASINH", makeMathWrapper("ASINH", math.Asinh))
@ -56,14 +55,12 @@ func init() {
RegisterFunction("_xlfn.FLOOR.PRECISE", FloorPrecise)
RegisterFunction("GCD", GCD)
RegisterFunction("INT", Int)
RegisterFunction("ISO.CEILING", CeilingPrecise) // appears to be the same from what I can tell
RegisterFunction("ISO.CEILING", CeilingPrecise)
RegisterFunction("LCM", LCM)
RegisterFunction("LN", makeMathWrapper("LN", math.Log))
RegisterFunction("LOG", Log)
RegisterFunction("LOG10", makeMathWrapper("LOG10", math.Log10))
RegisterFunction("MDETERM", MDeterm)
// RegisterFunction("MINVERSE", MInverse) // TODO: skipping the other matrix functinos, not sure how common they are
// RegisterFunction("MMULT"
RegisterFunction("MOD", Mod)
RegisterFunction("MROUND", Mround)
RegisterFunction("MULTINOMIAL", Multinomial)
@ -88,15 +85,11 @@ func init() {
RegisterFunction("SINH", makeMathWrapper("SINH", math.Sinh))
RegisterFunction("SQRT", makeMathWrapper("SQRT", math.Sqrt))
RegisterFunction("SQRTPI", makeMathWrapper("SQRTPI", func(v float64) float64 { return math.Sqrt(v * math.Pi) }))
// RegisterFunction("SUBTOTAL"
RegisterFunction("SUM", Sum)
RegisterFunction("SUMIF", SumIf)
RegisterFunction("SUMIFS", SumIfs)
RegisterFunction("SUMPRODUCT", SumProduct)
RegisterFunction("SUMSQ", SumSquares)
//RegisterFunction("SUMX2MY2"
//RegisterFunction("SUMX2PY2"
//RegisterFunction("SUMXMY2"
RegisterFunction("TAN", makeMathWrapper("TAN", math.Tan))
RegisterFunction("TANH", makeMathWrapper("TANH", math.Tanh))
RegisterFunction("TRUNC", Trunc)

View File

@ -18,42 +18,32 @@ import (
)
func init() {
// RegisterFunction("ASC") Need to figure out how to test
// RegisterFunction("BAHTTEXT")
RegisterFunction("CHAR", Char)
RegisterFunction("CLEAN", Clean)
RegisterFunction("CODE", Code)
RegisterFunction("CONCATENATE", Concat)
RegisterFunction("CONCAT", Concat)
RegisterFunction("_xlfn.CONCAT", Concat)
// RegisterFunction("DBCS")
// RegisterFunction("DOLLAR") Need to test with Excel
RegisterFunction("EXACT", Exact)
RegisterFunction("FIND", Find)
RegisterFunctionComplex("FINDB", Findb)
RegisterFunction("LEFT", Left)
RegisterFunction("LEFTB", Left) // for now
RegisterFunction("LEFTB", Left)
RegisterFunction("LEN", Len)
RegisterFunction("LENB", Len) // for now
RegisterFunction("LENB", Len)
RegisterFunction("LOWER", Lower)
// RegisterFunction("MID")
// RegisterFunction("MIDB")
// RegisterFunction("NUMBERVALUE")
// RegisterFunction("PHONETIC")
RegisterFunction("PROPER", Proper)
// RegisterFunction("REPLACE")
// RegisterFunction("REPLACEB")
RegisterFunction("REPLACE", Replace)
RegisterFunction("REPT", Rept)
RegisterFunction("RIGHT", Right)
RegisterFunction("RIGHTB", Right) // for now
RegisterFunction("RIGHTB", Right)
RegisterFunction("SEARCH", Search)
RegisterFunctionComplex("SEARCHB", Searchb)
//RegisterFunction("SUBSTITUTE", )
RegisterFunction("T", T)
//RegisterFunction("TEXT")
//RegisterFunction("TEXTJOIN")
RegisterFunction("TEXTJOIN", TextJoin)
RegisterFunction("_xlfn.TEXTJOIN", TextJoin)
RegisterFunction("TRIM", Trim)
RegisterFunction("_xlfn.UNICHAR", Char) // for now
RegisterFunction("_xlfn.UNICHAR", Char)
RegisterFunction("_xlfn.UNICODE", Unicode)
RegisterFunction("UPPER", Upper)
RegisterFunction("VALUE", Value)
@ -327,12 +317,64 @@ func Lower(args []Result) Result {
if len(args) != 1 {
return MakeErrorResult("LOWER requires a single string argument")
}
s := args[0].AsString()
arg := args[0]
switch arg.Type {
case ResultTypeError:
return arg
case ResultTypeNumber, ResultTypeString:
return lower(args[0])
case ResultTypeList:
list := arg.ValueList
resultList := []Result{}
for _, v := range list {
vLower := lower(v)
if vLower.Type == ResultTypeError {
return vLower
}
resultList = append(resultList, vLower)
}
return MakeListResult(resultList)
case ResultTypeArray:
array := arg.ValueArray
resultArray := [][]Result{}
for _, r := range array {
row := []Result{}
for _, v := range r {
vLower := lower(v)
if vLower.Type == ResultTypeError {
return vLower
}
row = append(row, vLower)
}
resultArray = append(resultArray, row)
}
return MakeArrayResult(resultArray)
default:
return MakeErrorResult("Incorrect argument for LOWER")
}
}
func lower(arg Result) Result {
if arg.Type == ResultTypeEmpty {
return arg
}
s := arg.AsString()
if s.Type != ResultTypeString {
return MakeErrorResult("LOWER requires a single string argument")
}
return MakeStringResult(strings.ToLower(s.ValueString))
if arg.IsBoolean {
if s.ValueString == "1" {
return MakeStringResult("true")
} else if s.ValueString == "0" {
return MakeStringResult("false")
} else {
return MakeErrorResult("Incorrect argument for LOWER")
}
} else {
return MakeStringResult(strings.ToLower(s.ValueString))
}
}
// Proper is an implementation of the Excel PROPER function that returns a copy
@ -571,3 +613,111 @@ func Value(args []Result) Result {
return MakeErrorResult("Incorrect argument for VALUE")
}
type parsedReplaceObject struct {
text string
startPos int
length int
textToReplace string
}
func parseReplaceResults(fname string, args []Result) (*parsedReplaceObject, Result) {
if len(args) != 4 {
return nil, MakeErrorResult(fname + " requires four arguments")
}
if args[0].Type != ResultTypeString {
return nil, MakeErrorResult(fname + " requires first argument to be a string")
}
text := args[0].ValueString
if args[1].Type != ResultTypeNumber {
return nil, MakeErrorResult(fname + " requires second argument to be a number")
}
startPos := int(args[1].ValueNumber) - 1
if args[2].Type != ResultTypeNumber {
return nil, MakeErrorResult(fname + " requires third argument to be a number")
}
length := int(args[2].ValueNumber)
if args[3].Type != ResultTypeString {
return nil, MakeErrorResult(fname + " requires fourth argument to be a string")
}
textToReplace := args[3].ValueString
return &parsedReplaceObject{
text,
startPos,
length,
textToReplace,
}, MakeEmptyResult()
}
// Replace is an implementation of the Excel REPLACE().
func Replace(args []Result) Result {
parsed, errResult := parseReplaceResults("REPLACE", args)
if errResult.Type != ResultTypeEmpty {
return errResult
}
text := parsed.text
startPos := parsed.startPos
length := parsed.length
textToReplace := parsed.textToReplace
textLen := len(text)
if startPos > textLen {
startPos = textLen
}
endPos := startPos + length
if endPos > textLen {
endPos = textLen
}
newText := text[0:startPos] + textToReplace + text[endPos:]
return MakeStringResult(newText)
}
// TextJoin is an implementation of the Excel TEXTJOIN function.
func TextJoin(args []Result) Result {
if len(args) < 3 {
return MakeErrorResult("TEXTJOIN requires three or more arguments")
}
if args[0].Type != ResultTypeString {
return MakeErrorResult("TEXTJOIN requires delimiter to be a string")
}
delimiter := args[0].ValueString
if args[1].Type != ResultTypeNumber {
return MakeErrorResult("TEXTJOIN requires second argument to be a number or boolean")
}
ignoreEmpty := args[1].ValueNumber != 0
arr := collectStrings(args[2:], []string{}, ignoreEmpty)
return MakeStringResult(strings.Join(arr, delimiter))
}
func collectStrings(args []Result, arr []string, ignoreEmpty bool) []string {
for _, result := range args {
switch result.Type {
case ResultTypeEmpty:
if !ignoreEmpty {
arr = append(arr, "")
}
case ResultTypeString:
if result.ValueString != "" || !ignoreEmpty {
arr = append(arr, result.ValueString)
}
case ResultTypeNumber:
arr = append(arr, result.Value())
case ResultTypeList:
arr = appendSlices(arr, collectStrings(result.ValueList, []string{}, ignoreEmpty))
case ResultTypeArray:
for _, row := range result.ValueArray {
arr = appendSlices(arr, collectStrings(row, []string{}, ignoreEmpty))
}
}
}
return arr
}
func appendSlices(s0, s1 []string) []string {
for _, item := range s1 {
s0 = append(s0, item)
}
return s0
}

View File

@ -1292,3 +1292,304 @@ func TestEomonth(t *testing.T) {
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestDuration(t *testing.T) {
ss := spreadsheet.New()
sheet := ss.AddSheet()
sheet.Cell("A1").SetTime(time.Date(2018, time.July, 1, 0, 0, 0, 0, time.UTC)) // settlement date
sheet.Cell("A2").SetTime(time.Date(2048, time.January, 1, 0, 0, 0, 0, time.UTC)) // maturity date
sheet.Cell("A3").SetNumber(0.08) // coupon rate
sheet.Cell("A4").SetNumber(0.09) // yield rate
sheet.Cell("A5").SetNumber(2) // frequency of payments
sheet.Cell("A6").SetNumber(0) // basis
sheet.Cell("A7").SetString("07/01/2018") // settlement date in string format
sheet.Cell("A8").SetString("01/01/2048") // maturity date in string format
td := []testStruct{
{`=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`},
{`=DURATION(A8,A7,A3,A4,A5,A6)`, `#NUM! ResultTypeError`},
}
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestMduration(t *testing.T) {
ss := spreadsheet.New()
sheet := ss.AddSheet()
sheet.Cell("A1").SetTime(time.Date(2008, time.January, 1, 0, 0, 0, 0, time.UTC)) // settlement date
sheet.Cell("A2").SetTime(time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC)) // maturity date
sheet.Cell("A3").SetNumber(0.08) // coupon rate
sheet.Cell("A4").SetNumber(0.09) // yield rate
sheet.Cell("A5").SetNumber(2) // frequency of payments
sheet.Cell("A6").SetNumber(0) // basis
sheet.Cell("A7").SetString("01/01/2008") // settlement date in string format
sheet.Cell("A8").SetString("01/01/2016") // maturity date in string format
td := []testStruct{
{`=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`},
{`=MDURATION(A8,A7,A3,A4,A5,A6)`, `#NUM! ResultTypeError`},
}
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestPduration(t *testing.T) {
ss := spreadsheet.New()
sheet := ss.AddSheet()
td := []testStruct{
{`=PDURATION(0.025,2000,2200)`, `3.85986616262 ResultTypeNumber`},
{`=PDURATION(0,2000,2200)`, `#NUM! ResultTypeError`},
{`=PDURATION(0.025,"2000",2200)`, `#VALUE! ResultTypeError`},
}
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestRow(t *testing.T) {
td := []testStruct{
{`=ROW(A1)`, `1 ResultTypeNumber`},
{`=ROW(A11)`, `11 ResultTypeNumber`},
{`=ROW(B11)`, `11 ResultTypeNumber`},
}
ss := spreadsheet.New()
sheet := ss.AddSheet()
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestRows(t *testing.T) {
td := []testStruct{
{`=ROWS(A1:E8)`, `8 ResultTypeNumber`},
{`=ROWS(E8:A1)`, `#VALUE! ResultTypeError`},
}
ss := spreadsheet.New()
sheet := ss.AddSheet()
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestLookup(t *testing.T) {
ss := spreadsheet.New()
sheet := ss.AddSheet()
sheet.Cell("A1").SetNumber(1)
sheet.Cell("A2").SetNumber(2)
sheet.Cell("A3").SetNumber(3)
sheet.Cell("B1").SetString("value1")
sheet.Cell("B2").SetString("value2")
sheet.Cell("B3").SetString("value3")
td := []testStruct{
{`=LOOKUP(2,A1:A3,B1:B3)`, `value2 ResultTypeString`},
{`=LOOKUP(2,A1:B1,A2:B2)`, `#N/A ResultTypeError`},
{`=LOOKUP(1,A1:B1,A2:B2)`, `2 ResultTypeNumber`},
}
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestVlookup(t *testing.T) {
ss := spreadsheet.New()
sheet := ss.AddSheet()
sheet.Cell("A1").SetNumber(100)
sheet.Cell("A2").SetNumber(200)
sheet.Cell("A3").SetNumber(300)
sheet.Cell("A4").SetNumber(400)
sheet.Cell("B1").SetString("value1")
sheet.Cell("B2").SetString("value2")
sheet.Cell("B3").SetString("value3")
sheet.Cell("B4").SetString("value4")
td := []testStruct{
{`=VLOOKUP(150,A1:B4,2)`, `value1 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`},
}
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestLarge(t *testing.T) {
ss := spreadsheet.New()
sheet := ss.AddSheet()
sheet.Cell("A1").SetNumber(400)
sheet.Cell("A2").SetNumber(300)
sheet.Cell("A3").SetNumber(500)
sheet.Cell("A4").SetNumber(100)
sheet.Cell("A5").SetNumber(200)
sheet.Cell("B1").SetString("abcde")
sheet.Cell("B2").SetNumber(150)
sheet.Cell("B3").SetNumber(350)
sheet.Cell("B4").SetNumber(450)
sheet.Cell("B5").SetNumber(250)
td := []testStruct{
{`=LARGE(A1:B5,4)`, `350 ResultTypeNumber`},
{`=LARGE(A1:B5,0)`, `#NUM! ResultTypeError`},
{`=LARGE(A1:B5,10)`, `#NUM! ResultTypeError`},
{`=LARGE(A2:B2,2)`, `150 ResultTypeNumber`},
}
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestSmall(t *testing.T) {
ss := spreadsheet.New()
sheet := ss.AddSheet()
sheet.Cell("A1").SetNumber(400)
sheet.Cell("A2").SetNumber(300)
sheet.Cell("A3").SetNumber(500)
sheet.Cell("A4").SetNumber(100)
sheet.Cell("A5").SetNumber(200)
sheet.Cell("B1").SetString("abcde")
sheet.Cell("B2").SetNumber(150)
sheet.Cell("B3").SetNumber(350)
sheet.Cell("B4").SetNumber(450)
sheet.Cell("B5").SetNumber(250)
td := []testStruct{
{`=SMALL(A1:B5,4)`, `250 ResultTypeNumber`},
{`=SMALL(A1:B5,0)`, `#NUM! ResultTypeError`},
{`=SMALL(A1:B5,10)`, `#NUM! ResultTypeError`},
{`=SMALL(A2:B2,2)`, `300 ResultTypeNumber`},
}
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestLower(t *testing.T) {
ss := spreadsheet.New()
sheet := ss.AddSheet()
sheet.Cell("A1").SetNumber(400)
sheet.Cell("A2").SetString("Hello")
sheet.Cell("B1").SetString("World!")
sheet.Cell("D1").SetFormulaArray("=LOWER(A1:B2)")
sheet.RecalculateFormulas()
td := []testStruct{
{`=D1`, `400 ResultTypeArray`},
{`=D2`, `hello ResultTypeString`},
{`=E1`, `world! ResultTypeString`},
{`=E2`, ` ResultTypeEmpty`},
}
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestReplace(t *testing.T) {
ss := spreadsheet.New()
sheet := ss.AddSheet()
sheet.Cell("A1").SetNumber(400)
sheet.Cell("A2").SetString("Hello")
sheet.Cell("B1").SetString("World!")
sheet.Cell("D1").SetFormulaArray("=LOWER(A1:B2)")
sheet.RecalculateFormulas()
td := []testStruct{
{`=REPLACE("Hello World!",7,5,"Earth")`, `Hello Earth! ResultTypeString`},
{`=REPLACE("Hello World!",7,10,"Earth")`, `Hello Earth ResultTypeString`},
{`=REPLACE("Hello World",30,10,"!")`, `Hello World! ResultTypeString`},
{`=REPLACE("Hello World!",7,0,"new ")`, `Hello new World! ResultTypeString`},
}
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestTextJoin(t *testing.T) {
ss := spreadsheet.New()
sheet := ss.AddSheet()
sheet.Cell("A1").SetNumber(1)
sheet.Cell("B1").SetString("2")
sheet.Cell("A2").SetNumber(3)
sheet.Cell("B2").SetString("4")
sheet.Cell("A3").SetNumber(5)
sheet.Cell("B3").SetString("6")
sheet.Cell("A4").SetString("7")
sheet.Cell("A6").SetString("8")
sheet.Cell("A7").SetString("9")
td := []testStruct{
{`=TEXTJOIN(".",FALSE,A1)`, `1 ResultTypeString`},
{`=TEXTJOIN("|",TRUE,A4:A7)`, `7|8|9 ResultTypeString`},
{`=TEXTJOIN("|",FALSE,A4:A7)`, `7||8|9 ResultTypeString`},
{`=TEXTJOIN(".",TRUE,A1:B2,A3:B3,A4,A5,A6,A7)`, `1.2.3.4.5.6.7.8.9 ResultTypeString`},
}
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}
func TestIndex(t *testing.T) {
ss := spreadsheet.New()
sheet := ss.AddSheet()
sheet.Cell("A1").SetNumber(1)
sheet.Cell("B1").SetString("2")
sheet.Cell("C1").SetNumber(3)
sheet.Cell("B2").SetNumber(5)
sheet.Cell("C2").SetString("6")
sheet.Cell("A3").SetString("7")
sheet.Cell("B3").SetString("8")
sheet.Cell("C3").SetString("9")
sheet.Cell("A4").SetFormulaRaw(`=INDEX(A1:C3,1,1)`)
sheet.Cell("A5").SetFormulaArray(`=INDEX(A1:C3,2)`)
sheet.Cell("A6").SetFormulaArray(`=INDEX(A1:C3,,2)`)
sheet.Cell("A10").SetFormulaArray(`=INDEX(A1:C3)`)
sheet.RecalculateFormulas()
td := []testStruct{
{`=A4`, `1 ResultTypeNumber`},
{`=A5`, `0 ResultTypeArray`},
{`=B5`, `5 ResultTypeNumber`},
{`=C5`, `6 ResultTypeNumber`},
{`=A7`, `5 ResultTypeNumber`},
{`=A8`, `8 ResultTypeNumber`},
{`=A10`, `#VALUE! ResultTypeError`},
}
ctx := sheet.FormulaContext()
runTests(t, ctx, td)
}

View File

@ -719,7 +719,9 @@ func (s *Sheet) setArray(origin string, arr formula.Result) error {
sr := s.Row(cref.RowIdx + uint32(ir))
for ic, val := range row {
cell := sr.Cell(reference.IndexToColumn(cref.ColumnIdx + uint32(ic)))
cell.SetCachedFormulaResult(val.String())
if val.Type != formula.ResultTypeEmpty {
cell.SetCachedFormulaResult(val.String())
}
}
}
return nil