Skip to content

Commit 8c97b95

Browse files
This closes qax-os#2165, add formula function UNIQUE support (qax-os#2166)
- Update unit tests --------- Co-authored-by: Ivan Hristov <[email protected]>
1 parent e799e95 commit 8c97b95

File tree

2 files changed

+227
-0
lines changed

2 files changed

+227
-0
lines changed

calc.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,7 @@ type formulaFuncs struct {
809809
// TYPE
810810
// UNICHAR
811811
// UNICODE
812+
// UNIQUE
812813
// UPPER
813814
// VALUE
814815
// VALUETOTEXT
@@ -14581,6 +14582,151 @@ func (fn *formulaFuncs) UNICODE(argsList *list.List) formulaArg {
1458114582
return fn.code("UNICODE", argsList)
1458214583
}
1458314584

14585+
// UNIQUE function returns a list of unique values in a list or range.
14586+
// For syntax refer to
14587+
// https://support.microsoft.com/en-us/office/unique-function-c5ab87fd-30a3-4ce9-9d1a-40204fb85e1e.
14588+
func (fn *formulaFuncs) UNIQUE(argsList *list.List) formulaArg {
14589+
args, errArg := getFormulaUniqueArgs(argsList)
14590+
if errArg != nil {
14591+
return *errArg
14592+
}
14593+
14594+
if args.byColumn {
14595+
args.cellRange, args.cols, args.rows = transposeFormulaArgsList(args.cellRange, args.cols, args.rows)
14596+
}
14597+
14598+
counts := map[string]int{}
14599+
14600+
for i := 0; i < len(args.cellRange); i += args.cols {
14601+
key := concatValues(args.cellRange[i : i+args.cols])
14602+
14603+
if _, ok := counts[key]; !ok {
14604+
counts[key] = 0
14605+
}
14606+
counts[key]++
14607+
}
14608+
14609+
uniqueAxes := [][]formulaArg{}
14610+
14611+
for i := 0; i < len(args.cellRange); i += args.cols {
14612+
key := concatValues(args.cellRange[i : i+args.cols])
14613+
14614+
if (args.exactlyOnce && counts[key] == 1) || (!args.exactlyOnce && counts[key] >= 1) {
14615+
uniqueAxes = append(uniqueAxes, args.cellRange[i:i+args.cols])
14616+
}
14617+
delete(counts, key)
14618+
}
14619+
14620+
if args.byColumn {
14621+
uniqueAxes = transposeFormulaArgsMatrix(uniqueAxes)
14622+
}
14623+
14624+
return newMatrixFormulaArg(uniqueAxes)
14625+
}
14626+
14627+
func transposeFormulaArgsMatrix(args [][]formulaArg) [][]formulaArg {
14628+
if len(args) == 0 {
14629+
return args
14630+
}
14631+
14632+
transposedArgs := make([][]formulaArg, len(args[0]))
14633+
14634+
for i := 0; i < len(args[0]); i++ {
14635+
transposedArgs[i] = make([]formulaArg, len(args))
14636+
}
14637+
14638+
for i := 0; i < len(args); i++ {
14639+
for j := 0; j < len(args[i]); j++ {
14640+
transposedArgs[j][i] = args[i][j]
14641+
}
14642+
}
14643+
14644+
return transposedArgs
14645+
}
14646+
14647+
func transposeFormulaArgsList(args []formulaArg, cols, rows int) ([]formulaArg, int, int) {
14648+
transposedArgs := make([]formulaArg, len(args))
14649+
14650+
for i := 0; i < rows; i++ {
14651+
for j := 0; j < cols; j++ {
14652+
transposedArgs[j*rows+i] = args[i*cols+j]
14653+
}
14654+
}
14655+
return transposedArgs, rows, cols
14656+
}
14657+
14658+
func concatValues(args []formulaArg) string {
14659+
val := ""
14660+
for _, arg := range args {
14661+
// Call to Value is cheap.
14662+
val += arg.Value()
14663+
}
14664+
return val
14665+
}
14666+
14667+
type uniqueArgs struct {
14668+
cellRange []formulaArg
14669+
cols int
14670+
rows int
14671+
byColumn bool
14672+
exactlyOnce bool
14673+
}
14674+
14675+
func getFormulaUniqueArgs(argsList *list.List) (uniqueArgs, *formulaArg) {
14676+
res := uniqueArgs{}
14677+
14678+
argsLen := argsList.Len()
14679+
if argsLen == 0 {
14680+
errArg := newErrorFormulaArg(formulaErrorVALUE, "UNIQUE requires at least 1 argument")
14681+
return res, &errArg
14682+
}
14683+
14684+
if argsLen > 3 {
14685+
msg := fmt.Sprintf("UNIQUE takes at most 3 arguments, received %d arguments", argsLen)
14686+
errArg := newErrorFormulaArg(formulaErrorVALUE, msg)
14687+
14688+
return res, &errArg
14689+
}
14690+
14691+
firstArg := argsList.Front()
14692+
res.cellRange = firstArg.Value.(formulaArg).ToList()
14693+
if len(res.cellRange) == 0 {
14694+
errArg := newErrorFormulaArg(formulaErrorVALUE, "missing first argument to UNIQUE")
14695+
return res, &errArg
14696+
}
14697+
if res.cellRange[0].Type == ArgError {
14698+
return res, &res.cellRange[0]
14699+
}
14700+
14701+
rmin, rmax := calcColsRowsMinMax(false, argsList)
14702+
cmin, cmax := calcColsRowsMinMax(true, argsList)
14703+
res.cols, res.rows = cmax-cmin+1, rmax-rmin+1
14704+
14705+
secondArg := firstArg.Next()
14706+
if secondArg == nil {
14707+
return res, nil
14708+
}
14709+
14710+
argByColumn := secondArg.Value.(formulaArg).ToBool()
14711+
if argByColumn.Type == ArgError {
14712+
return res, &argByColumn
14713+
}
14714+
res.byColumn = (argByColumn.Value() == "TRUE")
14715+
14716+
thirdArg := secondArg.Next()
14717+
if thirdArg == nil {
14718+
return res, nil
14719+
}
14720+
14721+
argExactlyOnce := thirdArg.Value.(formulaArg).ToBool()
14722+
if argExactlyOnce.Type == ArgError {
14723+
return res, &argExactlyOnce
14724+
}
14725+
res.exactlyOnce = (argExactlyOnce.Value() == "TRUE")
14726+
14727+
return res, nil
14728+
}
14729+
1458414730
// UPPER converts all characters in a supplied text string to upper case. The
1458514731
// syntax of the function is:
1458614732
//

calc_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1937,6 +1937,11 @@ func TestCalcCellValue(t *testing.T) {
19371937
"UNICODE(\"alpha\")": "97",
19381938
"UNICODE(\"?\")": "63",
19391939
"UNICODE(\"3\")": "51",
1940+
// UNIQUE
1941+
"TEXTJOIN(\",\", TRUE, UNIQUE(D2:D9))": "Jan,Feb",
1942+
"TEXTJOIN(\",\", TRUE, UNIQUE(D2:D9, FALSE, FALSE))": "Jan,Feb",
1943+
"TEXTJOIN(\",\", TRUE, UNIQUE(E2:E9, FALSE, FALSE))": "North 1,North 2,South 1,South 2",
1944+
"TEXTJOIN(\",\", TRUE, UNIQUE(D2:D9, FALSE, TRUE))": "",
19401945
// UPPER
19411946
"UPPER(\"test\")": "TEST",
19421947
"UPPER(\"TEST\")": "TEST",
@@ -5159,6 +5164,82 @@ func TestCalcCOVAR(t *testing.T) {
51595164
}
51605165
}
51615166

5167+
func TestCalcUniqueExactlyOnce(t *testing.T) {
5168+
cellData := [][]interface{}{
5169+
{"Customer name"},
5170+
{"Fife, Grant"},
5171+
{"Pruitt, Barbara"},
5172+
{"Horn, Frances"},
5173+
{"Barrett, Alicia"},
5174+
{"Barrett, Alicia"},
5175+
{"Larson, Lynn"},
5176+
{"Pruitt, Barbara"},
5177+
{"Snook, Anthony"},
5178+
{"Snook, Anthony"},
5179+
{"Horn, Frances"},
5180+
{"Brown, Charity"},
5181+
}
5182+
f := prepareCalcData(cellData)
5183+
5184+
formulaList := map[string]string{
5185+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:A12))": "Fife, Grant:Pruitt, Barbara:Horn, Frances:Barrett, Alicia:Larson, Lynn:Snook, Anthony:Brown, Charity",
5186+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:A12,FALSE,TRUE))": "Fife, Grant:Larson, Lynn:Brown, Charity",
5187+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:A12,FALSE,FALSE))": "Fife, Grant:Pruitt, Barbara:Horn, Frances:Barrett, Alicia:Larson, Lynn:Snook, Anthony:Brown, Charity",
5188+
}
5189+
for formula, expected := range formulaList {
5190+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
5191+
result, err := f.CalcCellValue("Sheet1", "C1")
5192+
assert.NoError(t, err, formula)
5193+
assert.Equal(t, expected, result, formula)
5194+
}
5195+
}
5196+
5197+
func TestCalcUniqueMultiColumn(t *testing.T) {
5198+
cellData := [][]interface{}{
5199+
{"Player name", "Gender", "Nickname"},
5200+
{"Tom", "M", "Tom"},
5201+
{"Fred", "M", "Fred"},
5202+
{"Amy", "F", "Amy"},
5203+
{"John", "M", "John"},
5204+
{"Malicia", "F", "Malicia"},
5205+
{"Fred", "M", "Fred"},
5206+
}
5207+
f := prepareCalcData(cellData)
5208+
5209+
formulaList := map[string]string{
5210+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:C7))": "Tom:M:Tom:Fred:M:Fred:Amy:F:Amy:John:M:John:Malicia:F:Malicia",
5211+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:C7,TRUE))": "Tom:M:Fred:M:Amy:F:John:M:Malicia:F:Fred:M",
5212+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:C7,TRUE, TRUE))": "M:M:F:M:F:M",
5213+
}
5214+
for formula, expected := range formulaList {
5215+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
5216+
result, err := f.CalcCellValue("Sheet1", "C1")
5217+
assert.NoError(t, err, formula)
5218+
assert.Equal(t, expected, result, formula)
5219+
}
5220+
}
5221+
5222+
func TestCalcUniqueErrors(t *testing.T) {
5223+
cellData := [][]interface{}{
5224+
{"Player name", "Gender", "Nickname"},
5225+
{"Tom", "M", "Tom"},
5226+
{"Fred", "M", "Fred"},
5227+
}
5228+
f := prepareCalcData(cellData)
5229+
formulaList := map[string]string{
5230+
"TEXTJOIN(\":\", TRUE, UNIQUE())": "#VALUE!",
5231+
"TEXTJOIN(\":\", TRUE, UNIQUE(1, 2, 3, 4))": "#VALUE!",
5232+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:A3, \"Hello\"))": "#VALUE!",
5233+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:A3, TRUE, \"Hello\"))": "#VALUE!",
5234+
}
5235+
for formula, expected := range formulaList {
5236+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
5237+
result, err := f.CalcCellValue("Sheet1", "C1")
5238+
assert.Error(t, err, formula)
5239+
assert.Equal(t, expected, result, formula)
5240+
}
5241+
}
5242+
51625243
func TestCalcDatabase(t *testing.T) {
51635244
cellData := [][]interface{}{
51645245
{"Tree", "Height", "Age", "Yield", "Profit", "Height"},

0 commit comments

Comments
 (0)