Skip to content

Commit fc4965c

Browse files
committed
feat(calc): add implicit intersection for Excel formulas
Implements Excel's implicit intersection behavior where matrix arguments to scalar functions resolve to single cells based on formula position. Functions like ABS and IF now correctly handle range references in non-array formulas. Fixes regex criteria anchoring to prevent partial string matches in SUMIF/COUNTIF functions. Patterns now anchor properly at runtime rather than during parsing.
1 parent 36f0011 commit fc4965c

2 files changed

Lines changed: 133 additions & 20 deletions

File tree

calc.go

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,27 @@ type formulaFuncs struct {
377377
sheet, cell string
378378
}
379379

380+
// implicitIntersect applies Excel's implicit intersection to a matrix argument.
381+
// For a non-array formula, when a whole-column or whole-row reference is passed
382+
// to a scalar function, Excel resolves it to the single cell in the same row
383+
// (or column) as the formula cell. If the argument is not a matrix, it is
384+
// returned unchanged.
385+
func (fn *formulaFuncs) implicitIntersect(arg formulaArg) formulaArg {
386+
if arg.Type != ArgMatrix {
387+
return arg
388+
}
389+
_, row, err := CellNameToCoordinates(fn.cell)
390+
if err != nil {
391+
return arg
392+
}
393+
// row is 1-based; matrix is 0-indexed
394+
idx := row - 1
395+
if idx >= 0 && idx < len(arg.Matrix) && len(arg.Matrix[idx]) > 0 {
396+
return arg.Matrix[idx][0]
397+
}
398+
return arg
399+
}
400+
380401
// CalcCellValue provides a function to get calculated cell value. This feature
381402
// is currently in working processing. Iterative calculation, implicit
382403
// intersection, explicit intersection, array formula, table formula and some
@@ -1862,7 +1883,7 @@ func formulaCriteriaParser(exp formulaArg) *formulaCriteria {
18621883
if strings.Contains(val, "*") {
18631884
val = strings.ReplaceAll(val, "*", ".*")
18641885
}
1865-
fc.Type, fc.Condition = criteriaRegexp, newStringFormulaArg("^"+val+"$")
1886+
fc.Type, fc.Condition = criteriaRegexp, newStringFormulaArg(val)
18661887
if num := fc.Condition.ToNumber(); num.Type == ArgNumber {
18671888
fc.Condition = num
18681889
}
@@ -1888,7 +1909,14 @@ func formulaCriteriaEval(val formulaArg, criteria *formulaCriteria) (result bool
18881909
}
18891910
}
18901911
case criteriaRegexp:
1891-
return regexp.MatchString(criteria.Condition.Value(), val.Value())
1912+
pattern := criteria.Condition.Value()
1913+
if !strings.HasPrefix(pattern, "^") {
1914+
pattern = "^" + pattern
1915+
}
1916+
if !strings.HasSuffix(pattern, "$") {
1917+
pattern = pattern + "$"
1918+
}
1919+
return regexp.MatchString(pattern, val.Value())
18921920
}
18931921
return
18941922
}
@@ -3695,7 +3723,7 @@ func (fn *formulaFuncs) ABS(argsList *list.List) formulaArg {
36953723
if argsList.Len() != 1 {
36963724
return newErrorFormulaArg(formulaErrorVALUE, "ABS requires 1 numeric argument")
36973725
}
3698-
arg := argsList.Front().Value.(formulaArg).ToNumber()
3726+
arg := fn.implicitIntersect(argsList.Front().Value.(formulaArg)).ToNumber()
36993727
if arg.Type == ArgError {
37003728
return arg
37013729
}
@@ -11688,20 +11716,7 @@ func (fn *formulaFuncs) ISNUMBER(argsList *list.List) formulaArg {
1168811716
return newErrorFormulaArg(formulaErrorVALUE, "ISNUMBER requires 1 argument")
1168911717
}
1169011718
arg := argsList.Front().Value.(formulaArg)
11691-
if arg.Type == ArgMatrix {
11692-
var mtx [][]formulaArg
11693-
for _, row := range arg.Matrix {
11694-
var array []formulaArg
11695-
for _, val := range row {
11696-
if val.Type == ArgNumber {
11697-
array = append(array, newBoolFormulaArg(true))
11698-
}
11699-
array = append(array, newBoolFormulaArg(false))
11700-
}
11701-
mtx = append(mtx, array)
11702-
}
11703-
return newMatrixFormulaArg(mtx)
11704-
}
11719+
arg = fn.implicitIntersect(arg)
1170511720
if arg.Type == ArgNumber {
1170611721
return newBoolFormulaArg(true)
1170711722
}
@@ -14844,7 +14859,7 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg {
1484414859
if argsList.Len() > 3 {
1484514860
return newErrorFormulaArg(formulaErrorVALUE, "IF accepts at most 3 arguments")
1484614861
}
14847-
token := argsList.Front().Value.(formulaArg)
14862+
token := fn.implicitIntersect(argsList.Front().Value.(formulaArg))
1484814863
var (
1484914864
cond bool
1485014865
err error
@@ -14863,7 +14878,7 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg {
1486314878
return newBoolFormulaArg(cond)
1486414879
}
1486514880
if cond {
14866-
value := argsList.Front().Next().Value.(formulaArg)
14881+
value := fn.implicitIntersect(argsList.Front().Next().Value.(formulaArg))
1486714882
switch value.Type {
1486814883
case ArgNumber:
1486914884
result = value.ToNumber()
@@ -14873,7 +14888,7 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg {
1487314888
return result
1487414889
}
1487514890
if argsList.Len() == 3 {
14876-
value := argsList.Back().Value.(formulaArg)
14891+
value := fn.implicitIntersect(argsList.Back().Value.(formulaArg))
1487714892
switch value.Type {
1487814893
case ArgNumber:
1487914894
result = value.ToNumber()

calc_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6856,3 +6856,101 @@ func TestCalcTrendGrowthRegression(t *testing.T) {
68566856
mtx := [][]float64{}
68576857
calcTrendGrowthRegression(false, false, 0, 0, 0, 0, 0, mtx, mtx, mtx, mtx)
68586858
}
6859+
6860+
func TestCalcImplicitIntersect(t *testing.T) {
6861+
f := NewFile()
6862+
assert.NoError(t, f.SetCellValue("Sheet1", "A1", -5))
6863+
assert.NoError(t, f.SetCellValue("Sheet1", "A2", 10))
6864+
assert.NoError(t, f.SetCellValue("Sheet1", "A3", -3))
6865+
assert.NoError(t, f.SetCellValue("Sheet1", "A4", "text"))
6866+
assert.NoError(t, f.SetCellValue("Sheet1", "A5", 7))
6867+
6868+
// ABS with range: formula in B1 resolves A1:A5 to A1 via implicit intersection
6869+
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "ABS(A1:A5)"))
6870+
result, err := f.CalcCellValue("Sheet1", "B1")
6871+
assert.NoError(t, err)
6872+
assert.Equal(t, "5", result, "ABS(A1:A5) in B1")
6873+
6874+
// ABS with range: formula in B2 resolves A1:A5 to A2 via implicit intersection
6875+
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", "ABS(A1:A5)"))
6876+
result, err = f.CalcCellValue("Sheet1", "B2")
6877+
assert.NoError(t, err)
6878+
assert.Equal(t, "10", result, "ABS(A1:A5) in B2")
6879+
6880+
// ABS with range: formula in B3 resolves A1:A5 to A3 via implicit intersection
6881+
assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "ABS(A1:A5)"))
6882+
result, err = f.CalcCellValue("Sheet1", "B3")
6883+
assert.NoError(t, err)
6884+
assert.Equal(t, "3", result, "ABS(A1:A5) in B3")
6885+
6886+
// ISNUMBER with range: formula in C1 resolves to A1 (number) -> TRUE
6887+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "ISNUMBER(A1:A5)"))
6888+
result, err = f.CalcCellValue("Sheet1", "C1")
6889+
assert.NoError(t, err)
6890+
assert.Equal(t, "TRUE", result, "ISNUMBER(A1:A5) in C1")
6891+
6892+
// ISNUMBER with range: formula in C4 resolves to A4 ("text") -> FALSE
6893+
assert.NoError(t, f.SetCellFormula("Sheet1", "C4", "ISNUMBER(A1:A5)"))
6894+
result, err = f.CalcCellValue("Sheet1", "C4")
6895+
assert.NoError(t, err)
6896+
assert.Equal(t, "FALSE", result, "ISNUMBER(A1:A5) in C4")
6897+
6898+
// ISNUMBER with range: formula in C5 resolves to A5 (number) -> TRUE
6899+
assert.NoError(t, f.SetCellFormula("Sheet1", "C5", "ISNUMBER(A1:A5)"))
6900+
result, err = f.CalcCellValue("Sheet1", "C5")
6901+
assert.NoError(t, err)
6902+
assert.Equal(t, "TRUE", result, "ISNUMBER(A1:A5) in C5")
6903+
6904+
// IF with range: condition resolved via implicit intersection
6905+
assert.NoError(t, f.SetCellValue("Sheet1", "D1", 1))
6906+
assert.NoError(t, f.SetCellValue("Sheet1", "D2", 0))
6907+
assert.NoError(t, f.SetCellValue("Sheet1", "D3", 1))
6908+
6909+
// Formula in E1: D1=1 (truthy) -> "yes"
6910+
assert.NoError(t, f.SetCellFormula("Sheet1", "E1", "IF(D1:D3,\"yes\",\"no\")"))
6911+
result, err = f.CalcCellValue("Sheet1", "E1")
6912+
assert.NoError(t, err)
6913+
assert.Equal(t, "yes", result, "IF(D1:D3,...) in E1")
6914+
6915+
// Formula in E2: D2=0 (falsy) -> "no"
6916+
assert.NoError(t, f.SetCellFormula("Sheet1", "E2", "IF(D1:D3,\"yes\",\"no\")"))
6917+
result, err = f.CalcCellValue("Sheet1", "E2")
6918+
assert.NoError(t, err)
6919+
assert.Equal(t, "no", result, "IF(D1:D3,...) in E2")
6920+
6921+
// Formula in E3: D3=1 (truthy) -> "yes"
6922+
assert.NoError(t, f.SetCellFormula("Sheet1", "E3", "IF(D1:D3,\"yes\",\"no\")"))
6923+
result, err = f.CalcCellValue("Sheet1", "E3")
6924+
assert.NoError(t, err)
6925+
assert.Equal(t, "yes", result, "IF(D1:D3,...) in E3")
6926+
}
6927+
6928+
func TestCalcCriteriaRegexpAnchoring(t *testing.T) {
6929+
cellData := [][]interface{}{
6930+
{"Category", "Amount"},
6931+
{"apple", 10},
6932+
{"pineapple", 20},
6933+
{"apple pie", 30},
6934+
{"APPLE", 40},
6935+
{5, 50},
6936+
{5.5, 60},
6937+
}
6938+
f := prepareCalcData(cellData)
6939+
for formula, expected := range map[string]string{
6940+
// Exact text match: only "apple" rows (case-insensitive in Excel)
6941+
`SUMIF(A2:A7,"apple",B2:B7)`: "10",
6942+
// Wildcard at start: matches "pineapple" and "apple"
6943+
`SUMIF(A2:A7,"*apple",B2:B7)`: "30",
6944+
// Wildcard at end: matches "apple" and "apple pie"
6945+
`SUMIF(A2:A7,"apple*",B2:B7)`: "40",
6946+
// Single char wildcard
6947+
`COUNTIF(A2:A7,"appl?")`: "1",
6948+
// Numeric criteria as exact match
6949+
`SUMIF(A2:A7,5,B2:B7)`: "50",
6950+
} {
6951+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
6952+
result, err := f.CalcCellValue("Sheet1", "C1")
6953+
assert.NoError(t, err, formula)
6954+
assert.Equal(t, expected, result, formula)
6955+
}
6956+
}

0 commit comments

Comments
 (0)