Skip to content

Commit b156de9

Browse files
Implement UNIQUE function (#5)
* Implement UNIQUE function * Reduce memory footprint --------- Co-authored-by: Ivan Hristov <[email protected]>
1 parent 55e152f commit b156de9

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
@@ -785,6 +785,7 @@ type formulaFuncs struct {
785785
// TYPE
786786
// UNICHAR
787787
// UNICODE
788+
// UNIQUE
788789
// UPPER
789790
// VALUE
790791
// VALUETOTEXT
@@ -14439,6 +14440,151 @@ func (fn *formulaFuncs) UNICODE(argsList *list.List) formulaArg {
1443914440
return fn.code("UNICODE", argsList)
1444014441
}
1444114442

14443+
// UNIQUE function returns a list of unique values in a list or range.
14444+
// For syntax refer to
14445+
// https://support.microsoft.com/en-us/office/unique-function-c5ab87fd-30a3-4ce9-9d1a-40204fb85e1e.
14446+
func (fn *formulaFuncs) UNIQUE(argsList *list.List) formulaArg {
14447+
args, errArg := getFormulaUniqueArgs(argsList)
14448+
if errArg != nil {
14449+
return *errArg
14450+
}
14451+
14452+
if args.byColumn {
14453+
args.cellRange, args.cols, args.rows = transposeFormulaArgsList(args.cellRange, args.cols, args.rows)
14454+
}
14455+
14456+
counts := map[string]int{}
14457+
14458+
for i := 0; i < len(args.cellRange); i += args.cols {
14459+
key := concatValues(args.cellRange[i : i+args.cols])
14460+
14461+
if _, ok := counts[key]; !ok {
14462+
counts[key] = 0
14463+
}
14464+
counts[key]++
14465+
}
14466+
14467+
uniqueAxes := [][]formulaArg{}
14468+
14469+
for i := 0; i < len(args.cellRange); i += args.cols {
14470+
key := concatValues(args.cellRange[i : i+args.cols])
14471+
14472+
if (args.exactlyOnce && counts[key] == 1) || (!args.exactlyOnce && counts[key] >= 1) {
14473+
uniqueAxes = append(uniqueAxes, args.cellRange[i:i+args.cols])
14474+
}
14475+
delete(counts, key)
14476+
}
14477+
14478+
if args.byColumn {
14479+
uniqueAxes = transposeFormulaArgsMatrix(uniqueAxes)
14480+
}
14481+
14482+
return newMatrixFormulaArg(uniqueAxes)
14483+
}
14484+
14485+
func transposeFormulaArgsMatrix(args [][]formulaArg) [][]formulaArg {
14486+
if len(args) == 0 {
14487+
return args
14488+
}
14489+
14490+
transposedArgs := make([][]formulaArg, len(args[0]))
14491+
14492+
for i := 0; i < len(args[0]); i++ {
14493+
transposedArgs[i] = make([]formulaArg, len(args))
14494+
}
14495+
14496+
for i := 0; i < len(args); i++ {
14497+
for j := 0; j < len(args[i]); j++ {
14498+
transposedArgs[j][i] = args[i][j]
14499+
}
14500+
}
14501+
14502+
return transposedArgs
14503+
}
14504+
14505+
func transposeFormulaArgsList(args []formulaArg, cols, rows int) ([]formulaArg, int, int) {
14506+
transposedArgs := make([]formulaArg, len(args))
14507+
14508+
for i := 0; i < rows; i++ {
14509+
for j := 0; j < cols; j++ {
14510+
transposedArgs[j*rows+i] = args[i*cols+j]
14511+
}
14512+
}
14513+
return transposedArgs, rows, cols
14514+
}
14515+
14516+
func concatValues(args []formulaArg) string {
14517+
val := ""
14518+
for _, arg := range args {
14519+
// Call to Value is cheap.
14520+
val += arg.Value()
14521+
}
14522+
return val
14523+
}
14524+
14525+
type uniqueArgs struct {
14526+
cellRange []formulaArg
14527+
cols int
14528+
rows int
14529+
byColumn bool
14530+
exactlyOnce bool
14531+
}
14532+
14533+
func getFormulaUniqueArgs(argsList *list.List) (uniqueArgs, *formulaArg) {
14534+
res := uniqueArgs{}
14535+
14536+
argsLen := argsList.Len()
14537+
if argsLen == 0 {
14538+
errArg := newErrorFormulaArg(formulaErrorVALUE, "UNIQUE requires at least 1 argument")
14539+
return res, &errArg
14540+
}
14541+
14542+
if argsLen > 3 {
14543+
msg := fmt.Sprintf("UNIQUE takes at most 3 arguments, received %d arguments", argsLen)
14544+
errArg := newErrorFormulaArg(formulaErrorVALUE, msg)
14545+
14546+
return res, &errArg
14547+
}
14548+
14549+
firstArg := argsList.Front()
14550+
res.cellRange = firstArg.Value.(formulaArg).ToList()
14551+
if len(res.cellRange) == 0 {
14552+
errArg := newErrorFormulaArg(formulaErrorVALUE, "missing first argument to UNIQUE")
14553+
return res, &errArg
14554+
}
14555+
if res.cellRange[0].Type == ArgError {
14556+
return res, &res.cellRange[0]
14557+
}
14558+
14559+
rmin, rmax := calcColsRowsMinMax(false, argsList)
14560+
cmin, cmax := calcColsRowsMinMax(true, argsList)
14561+
res.cols, res.rows = cmax-cmin+1, rmax-rmin+1
14562+
14563+
secondArg := firstArg.Next()
14564+
if secondArg == nil {
14565+
return res, nil
14566+
}
14567+
14568+
argByColumn := secondArg.Value.(formulaArg).ToBool()
14569+
if argByColumn.Type == ArgError {
14570+
return res, &argByColumn
14571+
}
14572+
res.byColumn = (argByColumn.Value() == "TRUE")
14573+
14574+
thirdArg := secondArg.Next()
14575+
if thirdArg == nil {
14576+
return res, nil
14577+
}
14578+
14579+
argExactlyOnce := thirdArg.Value.(formulaArg).ToBool()
14580+
if argExactlyOnce.Type == ArgError {
14581+
return res, &argExactlyOnce
14582+
}
14583+
res.exactlyOnce = (argExactlyOnce.Value() == "TRUE")
14584+
14585+
return res, nil
14586+
}
14587+
1444214588
// UPPER converts all characters in a supplied text string to upper case. The
1444314589
// syntax of the function is:
1444414590
//

calc_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,6 +1914,11 @@ func TestCalcCellValue(t *testing.T) {
19141914
"UNICODE(\"alpha\")": "97",
19151915
"UNICODE(\"?\")": "63",
19161916
"UNICODE(\"3\")": "51",
1917+
// UNIQUE
1918+
"TEXTJOIN(\",\", TRUE, UNIQUE(D2:D9))": "Jan,Feb",
1919+
"TEXTJOIN(\",\", TRUE, UNIQUE(D2:D9, FALSE, FALSE))": "Jan,Feb",
1920+
"TEXTJOIN(\",\", TRUE, UNIQUE(E2:E9, FALSE, FALSE))": "North 1,North 2,South 1,South 2",
1921+
"TEXTJOIN(\",\", TRUE, UNIQUE(D2:D9, FALSE, TRUE))": "",
19171922
// UPPER
19181923
"UPPER(\"test\")": "TEST",
19191924
"UPPER(\"TEST\")": "TEST",
@@ -5132,6 +5137,82 @@ func TestCalcCOVAR(t *testing.T) {
51325137
}
51335138
}
51345139

5140+
func TestCalcUniqueExactlyOnce(t *testing.T) {
5141+
cellData := [][]interface{}{
5142+
{"Customer name"},
5143+
{"Fife, Grant"},
5144+
{"Pruitt, Barbara"},
5145+
{"Horn, Frances"},
5146+
{"Barrett, Alicia"},
5147+
{"Barrett, Alicia"},
5148+
{"Larson, Lynn"},
5149+
{"Pruitt, Barbara"},
5150+
{"Snook, Anthony"},
5151+
{"Snook, Anthony"},
5152+
{"Horn, Frances"},
5153+
{"Brown, Charity"},
5154+
}
5155+
f := prepareCalcData(cellData)
5156+
5157+
formulaList := map[string]string{
5158+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:A12))": "Fife, Grant:Pruitt, Barbara:Horn, Frances:Barrett, Alicia:Larson, Lynn:Snook, Anthony:Brown, Charity",
5159+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:A12,FALSE,TRUE))": "Fife, Grant:Larson, Lynn:Brown, Charity",
5160+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:A12,FALSE,FALSE))": "Fife, Grant:Pruitt, Barbara:Horn, Frances:Barrett, Alicia:Larson, Lynn:Snook, Anthony:Brown, Charity",
5161+
}
5162+
for formula, expected := range formulaList {
5163+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
5164+
result, err := f.CalcCellValue("Sheet1", "C1")
5165+
assert.NoError(t, err, formula)
5166+
assert.Equal(t, expected, result, formula)
5167+
}
5168+
}
5169+
5170+
func TestCalcUniqueMultiColumn(t *testing.T) {
5171+
cellData := [][]interface{}{
5172+
{"Player name", "Gender", "Nickname"},
5173+
{"Tom", "M", "Tom"},
5174+
{"Fred", "M", "Fred"},
5175+
{"Amy", "F", "Amy"},
5176+
{"John", "M", "John"},
5177+
{"Malicia", "F", "Malicia"},
5178+
{"Fred", "M", "Fred"},
5179+
}
5180+
f := prepareCalcData(cellData)
5181+
5182+
formulaList := map[string]string{
5183+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:C7))": "Tom:M:Tom:Fred:M:Fred:Amy:F:Amy:John:M:John:Malicia:F:Malicia",
5184+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:C7,TRUE))": "Tom:M:Fred:M:Amy:F:John:M:Malicia:F:Fred:M",
5185+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:C7,TRUE, TRUE))": "M:M:F:M:F:M",
5186+
}
5187+
for formula, expected := range formulaList {
5188+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
5189+
result, err := f.CalcCellValue("Sheet1", "C1")
5190+
assert.NoError(t, err, formula)
5191+
assert.Equal(t, expected, result, formula)
5192+
}
5193+
}
5194+
5195+
func TestCalcUniqueErrors(t *testing.T) {
5196+
cellData := [][]interface{}{
5197+
{"Player name", "Gender", "Nickname"},
5198+
{"Tom", "M", "Tom"},
5199+
{"Fred", "M", "Fred"},
5200+
}
5201+
f := prepareCalcData(cellData)
5202+
formulaList := map[string]string{
5203+
"TEXTJOIN(\":\", TRUE, UNIQUE())": "#VALUE!",
5204+
"TEXTJOIN(\":\", TRUE, UNIQUE(1, 2, 3, 4))": "#VALUE!",
5205+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:A3, \"Hello\"))": "#VALUE!",
5206+
"TEXTJOIN(\":\", TRUE, UNIQUE(A2:A3, TRUE, \"Hello\"))": "#VALUE!",
5207+
}
5208+
for formula, expected := range formulaList {
5209+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
5210+
result, err := f.CalcCellValue("Sheet1", "C1")
5211+
assert.Error(t, err, formula)
5212+
assert.Equal(t, expected, result, formula)
5213+
}
5214+
}
5215+
51355216
func TestCalcDatabase(t *testing.T) {
51365217
cellData := [][]interface{}{
51375218
{"Tree", "Height", "Age", "Yield", "Profit", "Height"},

0 commit comments

Comments
 (0)