Skip to content

Commit

Permalink
This fixes #1643, fixes #1647 and fixes #1653
Browse files Browse the repository at this point in the history
- Correction cell type when formatting date type cell value
- Add check for MID and MIDB formula functions num_chars arguments, prevent panic on specifying a negative number
- Ref #65, add support for 2 formula functions: SEARCH and SEARCHB
- Fix a v2.8.0 regression bug, error on set print area and print titles with built-in special defined name
- Add new exported function `GetPivotTables` for get pivot tables
- Add a new `Name` field in the `PivotTableOptions` to support specify pivot table name
- Using relative cell reference in the pivot table docs and unit tests
- Support adding slicer content type part internally
- Add new exported source relationship and namespace `NameSpaceSpreadSheetXR10`, `ContentTypeSlicer`, `ContentTypeSlicerCache`, and `SourceRelationshipSlicer`
- Add new exported extended URI `ExtURIPivotCacheDefinition`
- Fix formula argument wildcard match issues
- Update GitHub Actions configuration, test on Go 1.21.x with 1.21.1 and later
- Avoid corrupted workbooks generated by improving compatibility with internally indexed color styles
  • Loading branch information
xuri committed Sep 7, 2023
1 parent ff5657b commit 5e8a020
Show file tree
Hide file tree
Showing 15 changed files with 504 additions and 159 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x]
go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, '>=1.21.1']
os: [ubuntu-latest, macos-latest, windows-latest]
targetplatform: [x86, x64]

Expand Down
187 changes: 129 additions & 58 deletions calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,8 @@ type formulaFuncs struct {
// ROWS
// RRI
// RSQ
// SEARCH
// SEARCHB
// SEC
// SECH
// SECOND
Expand Down Expand Up @@ -9303,7 +9305,7 @@ func (fn *formulaFuncs) FdotDISTdotRT(argsList *list.List) formulaArg {
return fn.FDIST(argsList)
}

// prepareFinvArgs checking and prepare arguments for the formula function
// prepareFinvArgs checking and prepare arguments for the formula functions
// F.INV, F.INV.RT and FINV.
func (fn *formulaFuncs) prepareFinvArgs(name string, argsList *list.List) formulaArg {
if argsList.Len() != 3 {
Expand Down Expand Up @@ -13612,17 +13614,16 @@ func (fn *formulaFuncs) FINDB(argsList *list.List) formulaArg {
return fn.find("FINDB", argsList)
}

// find is an implementation of the formula functions FIND and FINDB.
func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg {
// prepareFindArgs checking and prepare arguments for the formula functions
// FIND, FINDB, SEARCH and SEARCHB.
func (fn *formulaFuncs) prepareFindArgs(name string, argsList *list.List) formulaArg {
if argsList.Len() < 2 {
return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 2 arguments", name))
}
if argsList.Len() > 3 {
return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 3 arguments", name))
}
findText := argsList.Front().Value.(formulaArg).Value()
withinText := argsList.Front().Next().Value.(formulaArg).Value()
startNum, result := 1, 1
startNum := 1
if argsList.Len() == 3 {
numArg := argsList.Back().Value.(formulaArg).ToNumber()
if numArg.Type != ArgNumber {
Expand All @@ -13633,19 +13634,44 @@ func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg {
}
startNum = int(numArg.Number)
}
return newListFormulaArg([]formulaArg{newNumberFormulaArg(float64(startNum))})
}

// find is an implementation of the formula functions FIND, FINDB, SEARCH and
// SEARCHB.
func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg {
args := fn.prepareFindArgs(name, argsList)
if args.Type != ArgList {
return args
}
findText := argsList.Front().Value.(formulaArg).Value()
withinText := argsList.Front().Next().Value.(formulaArg).Value()
startNum := int(args.List[0].Number)
if findText == "" {
return newNumberFormulaArg(float64(startNum))
}
for idx := range withinText {
if result < startNum {
result++
}
if strings.Index(withinText[idx:], findText) == 0 {
return newNumberFormulaArg(float64(result))
dbcs, search := name == "FINDB" || name == "SEARCHB", name == "SEARCH" || name == "SEARCHB"
if search {
findText, withinText = strings.ToUpper(findText), strings.ToUpper(withinText)
}
offset, ok := matchPattern(findText, withinText, dbcs, startNum)
if !ok {
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
result := offset
if dbcs {
var pre int
for idx := range withinText {
if pre > offset {
break
}
if idx-pre > 1 {
result++
}
pre = idx
}
result++
}
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
return newNumberFormulaArg(float64(result))
}

// LEFT function returns a specified number of characters from the start of a
Expand Down Expand Up @@ -13780,20 +13806,37 @@ func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg {
return numCharsArg
}
startNum := int(startNumArg.Number)
if startNum < 0 {
if startNum < 1 || numCharsArg.Number < 0 {
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
if name == "MIDB" {
textLen := len(text)
if startNum > textLen {
return newStringFormulaArg("")
}
startNum--
endNum := startNum + int(numCharsArg.Number)
if endNum > textLen+1 {
return newStringFormulaArg(text[startNum:])
var result string
var cnt, offset int
for _, char := range text {
offset++
var dbcs bool
if utf8.RuneLen(char) > 1 {
dbcs = true
offset++
}
if cnt == int(numCharsArg.Number) {
break
}
if offset+1 > startNum {
if dbcs {
if cnt+2 > int(numCharsArg.Number) {
result += string(char)[:1]
break
}
result += string(char)
cnt += 2
} else {
result += string(char)
cnt++
}
}
}
return newStringFormulaArg(text[startNum:endNum])
return newStringFormulaArg(result)
}
// MID
textLen := utf8.RuneCountInString(text)
Expand Down Expand Up @@ -13922,6 +13965,23 @@ func (fn *formulaFuncs) RIGHTB(argsList *list.List) formulaArg {
return fn.leftRight("RIGHTB", argsList)
}

// SEARCH function returns the position of a specified character or sub-string
// within a supplied text string. The syntax of the function is:
//
// SEARCH(search_text,within_text,[start_num])
func (fn *formulaFuncs) SEARCH(argsList *list.List) formulaArg {
return fn.find("SEARCH", argsList)
}

// SEARCHB functions locate one text string within a second text string, and
// return the number of the starting position of the first text string from the
// first character of the second text string. The syntax of the function is:
//
// SEARCHB(search_text,within_text,[start_num])
func (fn *formulaFuncs) SEARCHB(argsList *list.List) formulaArg {
return fn.find("SEARCHB", argsList)
}

// SUBSTITUTE function replaces one or more instances of a given text string,
// within an original text string. The syntax of the function is:
//
Expand Down Expand Up @@ -14255,46 +14315,57 @@ func (fn *formulaFuncs) CHOOSE(argsList *list.List) formulaArg {
return arg.Value.(formulaArg)
}

// deepMatchRune finds whether the text deep matches/satisfies the pattern
// string.
func deepMatchRune(str, pattern []rune, simple bool) bool {
for len(pattern) > 0 {
switch pattern[0] {
default:
if len(str) == 0 || str[0] != pattern[0] {
return false
}
case '?':
if len(str) == 0 && !simple {
return false
}
case '*':
return deepMatchRune(str, pattern[1:], simple) ||
(len(str) > 0 && deepMatchRune(str[1:], pattern, simple))
// matchPatternToRegExp convert find text pattern to regular expression.
func matchPatternToRegExp(findText string, dbcs bool) (string, bool) {
var (
exp string
wildCard bool
mark = "."
)
if dbcs {
mark = "(?:(?:[\\x00-\\x0081])|(?:[\\xFF61-\\xFFA0])|(?:[\\xF8F1-\\xF8F4])|[0-9A-Za-z])"
}
for _, char := range findText {
if strings.ContainsAny(string(char), ".+$^[](){}|/") {
exp += fmt.Sprintf("\\%s", string(char))
continue
}
if char == '?' {
wildCard = true
exp += mark
continue
}
if char == '*' {
wildCard = true
exp += ".*"
continue
}
str = str[1:]
pattern = pattern[1:]
exp += string(char)
}
return len(str) == 0 && len(pattern) == 0
return fmt.Sprintf("^%s", exp), wildCard
}

// matchPattern finds whether the text matches or satisfies the pattern
// string. The pattern supports '*' and '?' wildcards in the pattern string.
func matchPattern(pattern, name string) (matched bool) {
if pattern == "" {
return name == pattern
}
if pattern == "*" {
return true
}
rName, rPattern := make([]rune, 0, len(name)), make([]rune, 0, len(pattern))
for _, r := range name {
rName = append(rName, r)
}
for _, r := range pattern {
rPattern = append(rPattern, r)
func matchPattern(findText, withinText string, dbcs bool, startNum int) (int, bool) {
exp, wildCard := matchPatternToRegExp(findText, dbcs)
offset := 1
for idx := range withinText {
if offset < startNum {
offset++
continue
}
if wildCard {
if ok, _ := regexp.MatchString(exp, withinText[idx:]); ok {
break
}
}
if strings.Index(withinText[idx:], findText) == 0 {
break
}
offset++
}
return deepMatchRune(rName, rPattern, false)
return offset, utf8.RuneCountInString(withinText) != offset-1
}

// compareFormulaArg compares the left-hand sides and the right-hand sides'
Expand All @@ -14319,7 +14390,7 @@ func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte
ls, rs = strings.ToLower(ls), strings.ToLower(rs)
}
if matchMode.Number == matchModeWildcard {
if matchPattern(rs, ls) {
if _, ok := matchPattern(rs, ls, false, 0); ok {
return criteriaEq
}
}
Expand Down
60 changes: 47 additions & 13 deletions calc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,30 @@ func TestCalcCellValue(t *testing.T) {
"=ROUNDUP(-11.111,2)": "-11.12",
"=ROUNDUP(-11.111,-1)": "-20",
"=ROUNDUP(ROUNDUP(100,1),-1)": "100",
// SEARCH
"=SEARCH(\"s\",F1)": "1",
"=SEARCH(\"s\",F1,2)": "5",
"=SEARCH(\"e\",F1)": "4",
"=SEARCH(\"e*\",F1)": "4",
"=SEARCH(\"?e\",F1)": "3",
"=SEARCH(\"??e\",F1)": "2",
"=SEARCH(6,F2)": "2",
"=SEARCH(\"?\",\"你好world\")": "1",
"=SEARCH(\"?l\",\"你好world\")": "5",
"=SEARCH(\"?+\",\"你好 1+2\")": "4",
"=SEARCH(\" ?+\",\"你好 1+2\")": "3",
// SEARCHB
"=SEARCHB(\"s\",F1)": "1",
"=SEARCHB(\"s\",F1,2)": "5",
"=SEARCHB(\"e\",F1)": "4",
"=SEARCHB(\"e*\",F1)": "4",
"=SEARCHB(\"?e\",F1)": "3",
"=SEARCHB(\"??e\",F1)": "2",
"=SEARCHB(6,F2)": "2",
"=SEARCHB(\"?\",\"你好world\")": "5",
"=SEARCHB(\"?l\",\"你好world\")": "7",
"=SEARCHB(\"?+\",\"你好 1+2\")": "6",
"=SEARCHB(\" ?+\",\"你好 1+2\")": "5",
// SEC
"=_xlfn.SEC(-3.14159265358979)": "-1",
"=_xlfn.SEC(0)": "1",
Expand Down Expand Up @@ -1707,13 +1731,15 @@ func TestCalcCellValue(t *testing.T) {
"=FIND(\"i\",\"Original Text\",4)": "5",
"=FIND(\"\",\"Original Text\")": "1",
"=FIND(\"\",\"Original Text\",2)": "2",
"=FIND(\"s\",\"Sales\",2)": "5",
// FINDB
"=FINDB(\"T\",\"Original Text\")": "10",
"=FINDB(\"t\",\"Original Text\")": "13",
"=FINDB(\"i\",\"Original Text\")": "3",
"=FINDB(\"i\",\"Original Text\",4)": "5",
"=FINDB(\"\",\"Original Text\")": "1",
"=FINDB(\"\",\"Original Text\",2)": "2",
"=FINDB(\"s\",\"Sales\",2)": "5",
// LEFT
"=LEFT(\"Original Text\")": "O",
"=LEFT(\"Original Text\",4)": "Orig",
Expand Down Expand Up @@ -1752,14 +1778,18 @@ func TestCalcCellValue(t *testing.T) {
"=MID(\"255 years\",3,1)": "5",
"=MID(\"text\",3,6)": "xt",
"=MID(\"text\",6,0)": "",
"=MID(\"オリジナルテキスト\",6,4)": "テキスト",
"=MID(\"オリジナルテキスト\",3,5)": "ジナルテキ",
"=MID(\"你好World\",5,1)": "r",
"=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30C6\u30AD\u30B9\u30C8",
"=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30B8\u30CA\u30EB\u30C6\u30AD",
// MIDB
"=MIDB(\"Original Text\",7,1)": "a",
"=MIDB(\"Original Text\",4,7)": "ginal T",
"=MIDB(\"255 years\",3,1)": "5",
"=MIDB(\"text\",3,6)": "xt",
"=MIDB(\"text\",6,0)": "",
"=MIDB(\"你好World\",5,1)": "W",
"=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30B8\u30CA",
"=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30EA\u30B8\xe3",
// PROPER
"=PROPER(\"this is a test sentence\")": "This Is A Test Sentence",
"=PROPER(\"THIS IS A TEST SENTENCE\")": "This Is A Test Sentence",
Expand Down Expand Up @@ -2695,6 +2725,17 @@ func TestCalcCellValue(t *testing.T) {
"=ROUNDUP()": {"#VALUE!", "ROUNDUP requires 2 numeric arguments"},
`=ROUNDUP("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
`=ROUNDUP(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// SEARCH
"=SEARCH()": {"#VALUE!", "SEARCH requires at least 2 arguments"},
"=SEARCH(1,A1,1,1)": {"#VALUE!", "SEARCH allows at most 3 arguments"},
"=SEARCH(2,A1)": {"#VALUE!", "#VALUE!"},
"=SEARCH(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
// SEARCHB
"=SEARCHB()": {"#VALUE!", "SEARCHB requires at least 2 arguments"},
"=SEARCHB(1,A1,1,1)": {"#VALUE!", "SEARCHB allows at most 3 arguments"},
"=SEARCHB(2,A1)": {"#VALUE!", "#VALUE!"},
"=SEARCHB(\"?w\",\"你好world\")": {"#VALUE!", "#VALUE!"},
"=SEARCHB(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
// SEC
"=_xlfn.SEC()": {"#VALUE!", "SEC requires 1 numeric argument"},
`=_xlfn.SEC("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
Expand Down Expand Up @@ -3781,12 +3822,14 @@ func TestCalcCellValue(t *testing.T) {
"=LOWER(1,2)": {"#VALUE!", "LOWER requires 1 argument"},
// MID
"=MID()": {"#VALUE!", "MID requires 3 arguments"},
"=MID(\"\",-1,1)": {"#VALUE!", "#VALUE!"},
"=MID(\"\",0,1)": {"#VALUE!", "#VALUE!"},
"=MID(\"\",1,-1)": {"#VALUE!", "#VALUE!"},
"=MID(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
"=MID(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
// MIDB
"=MIDB()": {"#VALUE!", "MIDB requires 3 arguments"},
"=MIDB(\"\",-1,1)": {"#VALUE!", "#VALUE!"},
"=MIDB(\"\",0,1)": {"#VALUE!", "#VALUE!"},
"=MIDB(\"\",1,-1)": {"#VALUE!", "#VALUE!"},
"=MIDB(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
"=MIDB(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
// PROPER
Expand Down Expand Up @@ -4684,14 +4727,6 @@ func TestCalcCompareFormulaArg(t *testing.T) {
assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, newNumberFormulaArg(matchModeMaxLess), false), criteriaErr)
}

func TestCalcMatchPattern(t *testing.T) {
assert.True(t, matchPattern("", ""))
assert.True(t, matchPattern("file/*", "file/abc/bcd/def"))
assert.True(t, matchPattern("*", ""))
assert.False(t, matchPattern("?", ""))
assert.False(t, matchPattern("file/?", "file/abc/bcd/def"))
}

func TestCalcTRANSPOSE(t *testing.T) {
cellData := [][]interface{}{
{"a", "d"},
Expand Down Expand Up @@ -5376,7 +5411,6 @@ func TestCalcXLOOKUP(t *testing.T) {
"=XLOOKUP()": {"#VALUE!", "XLOOKUP requires at least 3 arguments"},
"=XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,2,1)": {"#VALUE!", "XLOOKUP allows at most 6 arguments"},
"=XLOOKUP($C3,$C5,$C6,NA(),0,2)": {"#N/A", "#N/A"},
"=XLOOKUP(\"?\",B2:B9,C2:C9,NA(),2)": {"#N/A", "#N/A"},
"=XLOOKUP($C3,$C4:$D5,$C6:$C17,NA(),0,2)": {"#VALUE!", "#VALUE!"},
"=XLOOKUP($C3,$C5:$C5,$C6:$G17,NA(),0,-2)": {"#VALUE!", "#VALUE!"},
"=XLOOKUP($C3,$C5:$G5,$C6:$F7,NA(),0,2)": {"#VALUE!", "#VALUE!"},
Expand Down
Loading

0 comments on commit 5e8a020

Please sign in to comment.