Skip to content

Commit ae64bca

Browse files
committed
This fixes qax-os#1643, fixes qax-os#1647 and fixes qax-os#1653
- 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 qax-os#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
1 parent ff5657b commit ae64bca

15 files changed

+504
-159
lines changed

.github/workflows/go.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ jobs:
55
test:
66
strategy:
77
matrix:
8-
go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x]
8+
go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, '>=1.21.1']
99
os: [ubuntu-latest, macos-latest, windows-latest]
1010
targetplatform: [x86, x64]
1111

calc.go

+129-58
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,8 @@ type formulaFuncs struct {
706706
// ROWS
707707
// RRI
708708
// RSQ
709+
// SEARCH
710+
// SEARCHB
709711
// SEC
710712
// SECH
711713
// SECOND
@@ -9303,7 +9305,7 @@ func (fn *formulaFuncs) FdotDISTdotRT(argsList *list.List) formulaArg {
93039305
return fn.FDIST(argsList)
93049306
}
93059307

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

13615-
// find is an implementation of the formula functions FIND and FINDB.
13616-
func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg {
13617+
// prepareFindArgs checking and prepare arguments for the formula functions
13618+
// FIND, FINDB, SEARCH and SEARCHB.
13619+
func (fn *formulaFuncs) prepareFindArgs(name string, argsList *list.List) formulaArg {
1361713620
if argsList.Len() < 2 {
1361813621
return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 2 arguments", name))
1361913622
}
1362013623
if argsList.Len() > 3 {
1362113624
return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 3 arguments", name))
1362213625
}
13623-
findText := argsList.Front().Value.(formulaArg).Value()
13624-
withinText := argsList.Front().Next().Value.(formulaArg).Value()
13625-
startNum, result := 1, 1
13626+
startNum := 1
1362613627
if argsList.Len() == 3 {
1362713628
numArg := argsList.Back().Value.(formulaArg).ToNumber()
1362813629
if numArg.Type != ArgNumber {
@@ -13633,19 +13634,44 @@ func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg {
1363313634
}
1363413635
startNum = int(numArg.Number)
1363513636
}
13637+
return newListFormulaArg([]formulaArg{newNumberFormulaArg(float64(startNum))})
13638+
}
13639+
13640+
// find is an implementation of the formula functions FIND, FINDB, SEARCH and
13641+
// SEARCHB.
13642+
func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg {
13643+
args := fn.prepareFindArgs(name, argsList)
13644+
if args.Type != ArgList {
13645+
return args
13646+
}
13647+
findText := argsList.Front().Value.(formulaArg).Value()
13648+
withinText := argsList.Front().Next().Value.(formulaArg).Value()
13649+
startNum := int(args.List[0].Number)
1363613650
if findText == "" {
1363713651
return newNumberFormulaArg(float64(startNum))
1363813652
}
13639-
for idx := range withinText {
13640-
if result < startNum {
13641-
result++
13642-
}
13643-
if strings.Index(withinText[idx:], findText) == 0 {
13644-
return newNumberFormulaArg(float64(result))
13653+
dbcs, search := name == "FINDB" || name == "SEARCHB", name == "SEARCH" || name == "SEARCHB"
13654+
if search {
13655+
findText, withinText = strings.ToUpper(findText), strings.ToUpper(withinText)
13656+
}
13657+
offset, ok := matchPattern(findText, withinText, dbcs, startNum)
13658+
if !ok {
13659+
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
13660+
}
13661+
result := offset
13662+
if dbcs {
13663+
var pre int
13664+
for idx := range withinText {
13665+
if pre > offset {
13666+
break
13667+
}
13668+
if idx-pre > 1 {
13669+
result++
13670+
}
13671+
pre = idx
1364513672
}
13646-
result++
1364713673
}
13648-
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
13674+
return newNumberFormulaArg(float64(result))
1364913675
}
1365013676

1365113677
// LEFT function returns a specified number of characters from the start of a
@@ -13780,20 +13806,37 @@ func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg {
1378013806
return numCharsArg
1378113807
}
1378213808
startNum := int(startNumArg.Number)
13783-
if startNum < 0 {
13809+
if startNum < 1 || numCharsArg.Number < 0 {
1378413810
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
1378513811
}
1378613812
if name == "MIDB" {
13787-
textLen := len(text)
13788-
if startNum > textLen {
13789-
return newStringFormulaArg("")
13790-
}
13791-
startNum--
13792-
endNum := startNum + int(numCharsArg.Number)
13793-
if endNum > textLen+1 {
13794-
return newStringFormulaArg(text[startNum:])
13813+
var result string
13814+
var cnt, offset int
13815+
for _, char := range text {
13816+
offset++
13817+
var dbcs bool
13818+
if utf8.RuneLen(char) > 1 {
13819+
dbcs = true
13820+
offset++
13821+
}
13822+
if cnt == int(numCharsArg.Number) {
13823+
break
13824+
}
13825+
if offset+1 > startNum {
13826+
if dbcs {
13827+
if cnt+2 > int(numCharsArg.Number) {
13828+
result += string(char)[:1]
13829+
break
13830+
}
13831+
result += string(char)
13832+
cnt += 2
13833+
} else {
13834+
result += string(char)
13835+
cnt++
13836+
}
13837+
}
1379513838
}
13796-
return newStringFormulaArg(text[startNum:endNum])
13839+
return newStringFormulaArg(result)
1379713840
}
1379813841
// MID
1379913842
textLen := utf8.RuneCountInString(text)
@@ -13922,6 +13965,23 @@ func (fn *formulaFuncs) RIGHTB(argsList *list.List) formulaArg {
1392213965
return fn.leftRight("RIGHTB", argsList)
1392313966
}
1392413967

13968+
// SEARCH function returns the position of a specified character or sub-string
13969+
// within a supplied text string. The syntax of the function is:
13970+
//
13971+
// SEARCH(search_text,within_text,[start_num])
13972+
func (fn *formulaFuncs) SEARCH(argsList *list.List) formulaArg {
13973+
return fn.find("SEARCH", argsList)
13974+
}
13975+
13976+
// SEARCHB functions locate one text string within a second text string, and
13977+
// return the number of the starting position of the first text string from the
13978+
// first character of the second text string. The syntax of the function is:
13979+
//
13980+
// SEARCHB(search_text,within_text,[start_num])
13981+
func (fn *formulaFuncs) SEARCHB(argsList *list.List) formulaArg {
13982+
return fn.find("SEARCHB", argsList)
13983+
}
13984+
1392513985
// SUBSTITUTE function replaces one or more instances of a given text string,
1392613986
// within an original text string. The syntax of the function is:
1392713987
//
@@ -14255,46 +14315,57 @@ func (fn *formulaFuncs) CHOOSE(argsList *list.List) formulaArg {
1425514315
return arg.Value.(formulaArg)
1425614316
}
1425714317

14258-
// deepMatchRune finds whether the text deep matches/satisfies the pattern
14259-
// string.
14260-
func deepMatchRune(str, pattern []rune, simple bool) bool {
14261-
for len(pattern) > 0 {
14262-
switch pattern[0] {
14263-
default:
14264-
if len(str) == 0 || str[0] != pattern[0] {
14265-
return false
14266-
}
14267-
case '?':
14268-
if len(str) == 0 && !simple {
14269-
return false
14270-
}
14271-
case '*':
14272-
return deepMatchRune(str, pattern[1:], simple) ||
14273-
(len(str) > 0 && deepMatchRune(str[1:], pattern, simple))
14318+
// matchPatternToRegExp convert find text pattern to regular expression.
14319+
func matchPatternToRegExp(findText string, dbcs bool) (string, bool) {
14320+
var (
14321+
exp string
14322+
wildCard bool
14323+
mark = "."
14324+
)
14325+
if dbcs {
14326+
mark = "(?:(?:[\\x00-\\x0081])|(?:[\\xFF61-\\xFFA0])|(?:[\\xF8F1-\\xF8F4])|[0-9A-Za-z])"
14327+
}
14328+
for _, char := range findText {
14329+
if strings.ContainsAny(string(char), ".+$^[](){}|/") {
14330+
exp += fmt.Sprintf("\\%s", string(char))
14331+
continue
14332+
}
14333+
if char == '?' {
14334+
wildCard = true
14335+
exp += mark
14336+
continue
14337+
}
14338+
if char == '*' {
14339+
wildCard = true
14340+
exp += ".*"
14341+
continue
1427414342
}
14275-
str = str[1:]
14276-
pattern = pattern[1:]
14343+
exp += string(char)
1427714344
}
14278-
return len(str) == 0 && len(pattern) == 0
14345+
return fmt.Sprintf("^%s", exp), wildCard
1427914346
}
1428014347

1428114348
// matchPattern finds whether the text matches or satisfies the pattern
1428214349
// string. The pattern supports '*' and '?' wildcards in the pattern string.
14283-
func matchPattern(pattern, name string) (matched bool) {
14284-
if pattern == "" {
14285-
return name == pattern
14286-
}
14287-
if pattern == "*" {
14288-
return true
14289-
}
14290-
rName, rPattern := make([]rune, 0, len(name)), make([]rune, 0, len(pattern))
14291-
for _, r := range name {
14292-
rName = append(rName, r)
14293-
}
14294-
for _, r := range pattern {
14295-
rPattern = append(rPattern, r)
14350+
func matchPattern(findText, withinText string, dbcs bool, startNum int) (int, bool) {
14351+
exp, wildCard := matchPatternToRegExp(findText, dbcs)
14352+
offset := 1
14353+
for idx := range withinText {
14354+
if offset < startNum {
14355+
offset++
14356+
continue
14357+
}
14358+
if wildCard {
14359+
if ok, _ := regexp.MatchString(exp, withinText[idx:]); ok {
14360+
break
14361+
}
14362+
}
14363+
if strings.Index(withinText[idx:], findText) == 0 {
14364+
break
14365+
}
14366+
offset++
1429614367
}
14297-
return deepMatchRune(rName, rPattern, false)
14368+
return offset, utf8.RuneCountInString(withinText) != offset-1
1429814369
}
1429914370

1430014371
// compareFormulaArg compares the left-hand sides and the right-hand sides'
@@ -14319,7 +14390,7 @@ func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte
1431914390
ls, rs = strings.ToLower(ls), strings.ToLower(rs)
1432014391
}
1432114392
if matchMode.Number == matchModeWildcard {
14322-
if matchPattern(rs, ls) {
14393+
if _, ok := matchPattern(rs, ls, false, 0); ok {
1432314394
return criteriaEq
1432414395
}
1432514396
}

calc_test.go

+47-13
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,30 @@ func TestCalcCellValue(t *testing.T) {
764764
"=ROUNDUP(-11.111,2)": "-11.12",
765765
"=ROUNDUP(-11.111,-1)": "-20",
766766
"=ROUNDUP(ROUNDUP(100,1),-1)": "100",
767+
// SEARCH
768+
"=SEARCH(\"s\",F1)": "1",
769+
"=SEARCH(\"s\",F1,2)": "5",
770+
"=SEARCH(\"e\",F1)": "4",
771+
"=SEARCH(\"e*\",F1)": "4",
772+
"=SEARCH(\"?e\",F1)": "3",
773+
"=SEARCH(\"??e\",F1)": "2",
774+
"=SEARCH(6,F2)": "2",
775+
"=SEARCH(\"?\",\"你好world\")": "1",
776+
"=SEARCH(\"?l\",\"你好world\")": "5",
777+
"=SEARCH(\"?+\",\"你好 1+2\")": "4",
778+
"=SEARCH(\" ?+\",\"你好 1+2\")": "3",
779+
// SEARCHB
780+
"=SEARCHB(\"s\",F1)": "1",
781+
"=SEARCHB(\"s\",F1,2)": "5",
782+
"=SEARCHB(\"e\",F1)": "4",
783+
"=SEARCHB(\"e*\",F1)": "4",
784+
"=SEARCHB(\"?e\",F1)": "3",
785+
"=SEARCHB(\"??e\",F1)": "2",
786+
"=SEARCHB(6,F2)": "2",
787+
"=SEARCHB(\"?\",\"你好world\")": "5",
788+
"=SEARCHB(\"?l\",\"你好world\")": "7",
789+
"=SEARCHB(\"?+\",\"你好 1+2\")": "6",
790+
"=SEARCHB(\" ?+\",\"你好 1+2\")": "5",
767791
// SEC
768792
"=_xlfn.SEC(-3.14159265358979)": "-1",
769793
"=_xlfn.SEC(0)": "1",
@@ -1707,13 +1731,15 @@ func TestCalcCellValue(t *testing.T) {
17071731
"=FIND(\"i\",\"Original Text\",4)": "5",
17081732
"=FIND(\"\",\"Original Text\")": "1",
17091733
"=FIND(\"\",\"Original Text\",2)": "2",
1734+
"=FIND(\"s\",\"Sales\",2)": "5",
17101735
// FINDB
17111736
"=FINDB(\"T\",\"Original Text\")": "10",
17121737
"=FINDB(\"t\",\"Original Text\")": "13",
17131738
"=FINDB(\"i\",\"Original Text\")": "3",
17141739
"=FINDB(\"i\",\"Original Text\",4)": "5",
17151740
"=FINDB(\"\",\"Original Text\")": "1",
17161741
"=FINDB(\"\",\"Original Text\",2)": "2",
1742+
"=FINDB(\"s\",\"Sales\",2)": "5",
17171743
// LEFT
17181744
"=LEFT(\"Original Text\")": "O",
17191745
"=LEFT(\"Original Text\",4)": "Orig",
@@ -1752,14 +1778,18 @@ func TestCalcCellValue(t *testing.T) {
17521778
"=MID(\"255 years\",3,1)": "5",
17531779
"=MID(\"text\",3,6)": "xt",
17541780
"=MID(\"text\",6,0)": "",
1755-
"=MID(\"オリジナルテキスト\",6,4)": "テキスト",
1756-
"=MID(\"オリジナルテキスト\",3,5)": "ジナルテキ",
1781+
"=MID(\"你好World\",5,1)": "r",
1782+
"=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30C6\u30AD\u30B9\u30C8",
1783+
"=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30B8\u30CA\u30EB\u30C6\u30AD",
17571784
// MIDB
17581785
"=MIDB(\"Original Text\",7,1)": "a",
17591786
"=MIDB(\"Original Text\",4,7)": "ginal T",
17601787
"=MIDB(\"255 years\",3,1)": "5",
17611788
"=MIDB(\"text\",3,6)": "xt",
17621789
"=MIDB(\"text\",6,0)": "",
1790+
"=MIDB(\"你好World\",5,1)": "W",
1791+
"=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30B8\u30CA",
1792+
"=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30EA\u30B8\xe3",
17631793
// PROPER
17641794
"=PROPER(\"this is a test sentence\")": "This Is A Test Sentence",
17651795
"=PROPER(\"THIS IS A TEST SENTENCE\")": "This Is A Test Sentence",
@@ -2695,6 +2725,17 @@ func TestCalcCellValue(t *testing.T) {
26952725
"=ROUNDUP()": {"#VALUE!", "ROUNDUP requires 2 numeric arguments"},
26962726
`=ROUNDUP("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
26972727
`=ROUNDUP(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
2728+
// SEARCH
2729+
"=SEARCH()": {"#VALUE!", "SEARCH requires at least 2 arguments"},
2730+
"=SEARCH(1,A1,1,1)": {"#VALUE!", "SEARCH allows at most 3 arguments"},
2731+
"=SEARCH(2,A1)": {"#VALUE!", "#VALUE!"},
2732+
"=SEARCH(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
2733+
// SEARCHB
2734+
"=SEARCHB()": {"#VALUE!", "SEARCHB requires at least 2 arguments"},
2735+
"=SEARCHB(1,A1,1,1)": {"#VALUE!", "SEARCHB allows at most 3 arguments"},
2736+
"=SEARCHB(2,A1)": {"#VALUE!", "#VALUE!"},
2737+
"=SEARCHB(\"?w\",\"你好world\")": {"#VALUE!", "#VALUE!"},
2738+
"=SEARCHB(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
26982739
// SEC
26992740
"=_xlfn.SEC()": {"#VALUE!", "SEC requires 1 numeric argument"},
27002741
`=_xlfn.SEC("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
@@ -3781,12 +3822,14 @@ func TestCalcCellValue(t *testing.T) {
37813822
"=LOWER(1,2)": {"#VALUE!", "LOWER requires 1 argument"},
37823823
// MID
37833824
"=MID()": {"#VALUE!", "MID requires 3 arguments"},
3784-
"=MID(\"\",-1,1)": {"#VALUE!", "#VALUE!"},
3825+
"=MID(\"\",0,1)": {"#VALUE!", "#VALUE!"},
3826+
"=MID(\"\",1,-1)": {"#VALUE!", "#VALUE!"},
37853827
"=MID(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
37863828
"=MID(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
37873829
// MIDB
37883830
"=MIDB()": {"#VALUE!", "MIDB requires 3 arguments"},
3789-
"=MIDB(\"\",-1,1)": {"#VALUE!", "#VALUE!"},
3831+
"=MIDB(\"\",0,1)": {"#VALUE!", "#VALUE!"},
3832+
"=MIDB(\"\",1,-1)": {"#VALUE!", "#VALUE!"},
37903833
"=MIDB(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
37913834
"=MIDB(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
37923835
// PROPER
@@ -4684,14 +4727,6 @@ func TestCalcCompareFormulaArg(t *testing.T) {
46844727
assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, newNumberFormulaArg(matchModeMaxLess), false), criteriaErr)
46854728
}
46864729

4687-
func TestCalcMatchPattern(t *testing.T) {
4688-
assert.True(t, matchPattern("", ""))
4689-
assert.True(t, matchPattern("file/*", "file/abc/bcd/def"))
4690-
assert.True(t, matchPattern("*", ""))
4691-
assert.False(t, matchPattern("?", ""))
4692-
assert.False(t, matchPattern("file/?", "file/abc/bcd/def"))
4693-
}
4694-
46954730
func TestCalcTRANSPOSE(t *testing.T) {
46964731
cellData := [][]interface{}{
46974732
{"a", "d"},
@@ -5376,7 +5411,6 @@ func TestCalcXLOOKUP(t *testing.T) {
53765411
"=XLOOKUP()": {"#VALUE!", "XLOOKUP requires at least 3 arguments"},
53775412
"=XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,2,1)": {"#VALUE!", "XLOOKUP allows at most 6 arguments"},
53785413
"=XLOOKUP($C3,$C5,$C6,NA(),0,2)": {"#N/A", "#N/A"},
5379-
"=XLOOKUP(\"?\",B2:B9,C2:C9,NA(),2)": {"#N/A", "#N/A"},
53805414
"=XLOOKUP($C3,$C4:$D5,$C6:$C17,NA(),0,2)": {"#VALUE!", "#VALUE!"},
53815415
"=XLOOKUP($C3,$C5:$C5,$C6:$G17,NA(),0,-2)": {"#VALUE!", "#VALUE!"},
53825416
"=XLOOKUP($C3,$C5:$G5,$C6:$F7,NA(),0,2)": {"#VALUE!", "#VALUE!"},

0 commit comments

Comments
 (0)