Skip to content

Commit 37b6a8f

Browse files
authored
Fix formula exact text matching not match substrings (#2267)
- Update unit test - Add tilde wildcard criteria support
1 parent 3e9bc14 commit 37b6a8f

2 files changed

Lines changed: 77 additions & 6 deletions

File tree

calc.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ const (
101101
)
102102

103103
var (
104+
// wildcardTokenRE tokenizes an Excel wildcard pattern into tilde-escaped
105+
// sequences, bare wildcards (* ?), or any other single character.
106+
wildcardTokenRE = regexp.MustCompile(`~[*?~]|[*?]|[\s\S]`)
107+
// wildcardPatternMap maps each token produced by wildcardTokenRE to its
108+
// regular-expression equivalent. Tokens absent from the map are literals.
109+
wildcardPatternMap = map[string]string{
110+
"~*": regexp.QuoteMeta("*"),
111+
"~?": regexp.QuoteMeta("?"),
112+
"~~": regexp.QuoteMeta("~"),
113+
"*": ".*",
114+
"?": ".",
115+
}
116+
// wildcardBareTokens is the set of tokens that represent real wildcards.
117+
wildcardBareTokens = map[string]bool{"*": true, "?": true}
104118
// tokenPriority defined basic arithmetic operator priority
105119
tokenPriority = map[string]int{
106120
"^": 5,
@@ -1184,12 +1198,20 @@ func calcPow(rOpd, lOpd formulaArg, opdStack *Stack) error {
11841198

11851199
// calcEq evaluate equal arithmetic operations.
11861200
func calcEq(rOpd, lOpd formulaArg, opdStack *Stack) error {
1201+
if rOpd.Type == ArgString && lOpd.Type == ArgString {
1202+
opdStack.Push(newBoolFormulaArg(strings.EqualFold(lOpd.Value(), rOpd.Value())))
1203+
return nil
1204+
}
11871205
opdStack.Push(newBoolFormulaArg(rOpd.Value() == lOpd.Value()))
11881206
return nil
11891207
}
11901208

11911209
// calcNEq evaluate not equal arithmetic operations.
11921210
func calcNEq(rOpd, lOpd formulaArg, opdStack *Stack) error {
1211+
if rOpd.Type == ArgString && lOpd.Type == ArgString {
1212+
opdStack.Push(newBoolFormulaArg(!strings.EqualFold(lOpd.Value(), rOpd.Value())))
1213+
return nil
1214+
}
11931215
opdStack.Push(newBoolFormulaArg(rOpd.Value() != lOpd.Value()))
11941216
return nil
11951217
}
@@ -1860,13 +1882,21 @@ func formulaCriteriaParser(exp formulaArg) *formulaCriteria {
18601882
return fc
18611883
}
18621884
}
1863-
if strings.Contains(val, "?") {
1864-
val = strings.ReplaceAll(val, "?", ".")
1865-
}
1866-
if strings.Contains(val, "*") {
1867-
val = strings.ReplaceAll(val, "*", ".*")
1885+
hasWildcard := false
1886+
pattern := wildcardTokenRE.ReplaceAllStringFunc(val, func(m string) string {
1887+
hasWildcard = hasWildcard || wildcardBareTokens[m]
1888+
if r, ok := wildcardPatternMap[m]; ok {
1889+
return r
1890+
}
1891+
return regexp.QuoteMeta(m)
1892+
})
1893+
if hasWildcard {
1894+
fc.Type, fc.Condition = criteriaRegexp, newStringFormulaArg("(?i)^"+pattern+"$")
1895+
return fc
18681896
}
1869-
fc.Type, fc.Condition = criteriaRegexp, newStringFormulaArg(val)
1897+
fc.Type, fc.Condition = criteriaEq, newStringFormulaArg(
1898+
strings.NewReplacer("~~", "~", "~*", "*", "~?", "?").Replace(val),
1899+
)
18701900
if num := fc.Condition.ToNumber(); num.Type == ArgNumber {
18711901
fc.Condition = num
18721902
}

calc_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5739,6 +5739,47 @@ func TestCalcSUMIFSAndAVERAGEIFS(t *testing.T) {
57395739
}
57405740
}
57415741

5742+
func TestCalcSUMIFExactMatch(t *testing.T) {
5743+
cellData := [][]interface{}{
5744+
{"Category", "Amount"},
5745+
{"text", 100},
5746+
{"***_***_text", 200},
5747+
{"text", 150},
5748+
{"***_text_***", 300},
5749+
{"TEXT", 50},
5750+
{"other", 400},
5751+
}
5752+
f := prepareCalcData(cellData)
5753+
formulaList := map[string]string{
5754+
`SUMIF(A2:A7,"text",B2:B7)`: "300",
5755+
`COUNTIF(A2:A7,"text")`: "3",
5756+
`SUMIF(A2:A7,".",B2:B7)`: "0",
5757+
`SUMIF(A2:A7,".*",B2:B7)`: "0",
5758+
`SUMIF(A2:A7,".?",B2:B7)`: "0",
5759+
`SUMIF(A2:A7,"*text*",B2:B7)`: "800",
5760+
`SUMIF(A2:A7,"text*",B2:B7)`: "300",
5761+
`SUMIF(A2:A7,"*text",B2:B7)`: "500",
5762+
`SUMIF(A2:A7,"*",B2:B7)`: "1200",
5763+
`SUMIF(A2:A7,"othe?",B2:B7)`: "400",
5764+
`SUMIF(A2:A7,"~**",B2:B7)`: "500",
5765+
`COUNTIF(A2:A7,"*text*")`: "5",
5766+
`COUNTIF(A2:A7,"~*")`: "0",
5767+
`COUNTIF(A2:A7,"~~")`: "0",
5768+
`COUNTIF(A2:A7,"~?")`: "0",
5769+
`COUNTIF(A2:A7,"*~**")`: "2",
5770+
`COUNTIF(A2:A7,"~*~*~*_~*~*~*_text")`: "1",
5771+
`COUNTIF(A2:A7,"~*~*~*_text_~*~*~*")`: "1",
5772+
`COUNTIF(A2:A7,"~a")`: "0",
5773+
`COUNTIF(A2:A7,"<>text")`: "3",
5774+
}
5775+
for formula, expected := range formulaList {
5776+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
5777+
result, err := f.CalcCellValue("Sheet1", "C1")
5778+
assert.NoError(t, err, formula)
5779+
assert.Equal(t, expected, result, formula)
5780+
}
5781+
}
5782+
57425783
func TestCalcXIRR(t *testing.T) {
57435784
cellData := [][]interface{}{
57445785
{-100.00, 42370},

0 commit comments

Comments
 (0)