Skip to content

Commit 14c6a19

Browse files
committed
Support get cell value which contains a date in the ISO 8601 format
- Support set and get font color with indexed color - New export variable `IndexedColorMapping` - Fix getting incorrect page margin settings when the margin is 0 - Update unit tests and comments typo fixes - ref qax-os#65, new formula functions: AGGREGATE and SUBTOTAL
1 parent f843a9e commit 14c6a19

15 files changed

+294
-116
lines changed

calc.go

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ type formulaFuncs struct {
339339
// ACOT
340340
// ACOTH
341341
// ADDRESS
342+
// AGGREGATE
342343
// AMORDEGRC
343344
// AMORLINC
344345
// AND
@@ -700,6 +701,7 @@ type formulaFuncs struct {
700701
// STDEVPA
701702
// STEYX
702703
// SUBSTITUTE
704+
// SUBTOTAL
703705
// SUM
704706
// SUMIF
705707
// SUMIFS
@@ -872,7 +874,6 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T
872874
var err error
873875
opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack()
874876
var inArray, inArrayRow bool
875-
var arrayRow []formulaArg
876877
for i := 0; i < len(tokens); i++ {
877878
token := tokens[i]
878879

@@ -981,7 +982,6 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T
981982
argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(token.TValue))
982983
}
983984
if inArrayRow && isOperand(token) {
984-
arrayRow = append(arrayRow, tokenToFormulaArg(token))
985985
continue
986986
}
987987
if inArrayRow && isFunctionStopToken(token) {
@@ -990,7 +990,7 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T
990990
}
991991
if inArray && isFunctionStopToken(token) {
992992
argsStack.Peek().(*list.List).PushBack(opfdStack.Pop())
993-
arrayRow, inArray = []formulaArg{}, false
993+
inArray = false
994994
continue
995995
}
996996
if err = f.evalInfixExpFunc(ctx, sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil {
@@ -3559,6 +3559,56 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) formulaArg {
35593559
return newNumberFormulaArg(math.Atanh(1 / arg.Number))
35603560
}
35613561

3562+
// AGGREGATE function returns the result of a specified operation or function,
3563+
// applied to a list or database of values. The syntax of the function is:
3564+
//
3565+
// AGGREGATE(function_num,options,ref1,[ref2],...)
3566+
func (fn *formulaFuncs) AGGREGATE(argsList *list.List) formulaArg {
3567+
if argsList.Len() < 2 {
3568+
return newErrorFormulaArg(formulaErrorVALUE, "AGGREGATE requires at least 3 arguments")
3569+
}
3570+
var fnNum, opts formulaArg
3571+
if fnNum = argsList.Front().Value.(formulaArg).ToNumber(); fnNum.Type != ArgNumber {
3572+
return fnNum
3573+
}
3574+
subFn, ok := map[int]func(argsList *list.List) formulaArg{
3575+
1: fn.AVERAGE,
3576+
2: fn.COUNT,
3577+
3: fn.COUNTA,
3578+
4: fn.MAX,
3579+
5: fn.MIN,
3580+
6: fn.PRODUCT,
3581+
7: fn.STDEVdotS,
3582+
8: fn.STDEVdotP,
3583+
9: fn.SUM,
3584+
10: fn.VARdotS,
3585+
11: fn.VARdotP,
3586+
12: fn.MEDIAN,
3587+
13: fn.MODEdotSNGL,
3588+
14: fn.LARGE,
3589+
15: fn.SMALL,
3590+
16: fn.PERCENTILEdotINC,
3591+
17: fn.QUARTILEdotINC,
3592+
18: fn.PERCENTILEdotEXC,
3593+
19: fn.QUARTILEdotEXC,
3594+
}[int(fnNum.Number)]
3595+
if !ok {
3596+
return newErrorFormulaArg(formulaErrorVALUE, "AGGREGATE has invalid function_num")
3597+
}
3598+
if opts = argsList.Front().Next().Value.(formulaArg).ToNumber(); opts.Type != ArgNumber {
3599+
return opts
3600+
}
3601+
// TODO: apply option argument values to be ignored during the calculation
3602+
if int(opts.Number) < 0 || int(opts.Number) > 7 {
3603+
return newErrorFormulaArg(formulaErrorVALUE, "AGGREGATE has invalid options")
3604+
}
3605+
subArgList := list.New().Init()
3606+
for arg := argsList.Front().Next().Next(); arg != nil; arg = arg.Next() {
3607+
subArgList.PushBack(arg.Value.(formulaArg))
3608+
}
3609+
return subFn(subArgList)
3610+
}
3611+
35623612
// ARABIC function converts a Roman numeral into an Arabic numeral. The syntax
35633613
// of the function is:
35643614
//
@@ -5555,6 +5605,41 @@ func (fn *formulaFuncs) POISSON(argsList *list.List) formulaArg {
55555605
return newNumberFormulaArg(math.Exp(0-mean.Number) * math.Pow(mean.Number, x.Number) / fact(x.Number))
55565606
}
55575607

5608+
// SUBTOTAL function performs a specified calculation (e.g. the sum, product,
5609+
// average, etc.) for a supplied set of values. The syntax of the function is:
5610+
//
5611+
// SUBTOTAL(function_num,ref1,[ref2],...)
5612+
func (fn *formulaFuncs) SUBTOTAL(argsList *list.List) formulaArg {
5613+
if argsList.Len() < 2 {
5614+
return newErrorFormulaArg(formulaErrorVALUE, "SUBTOTAL requires at least 2 arguments")
5615+
}
5616+
var fnNum formulaArg
5617+
if fnNum = argsList.Front().Value.(formulaArg).ToNumber(); fnNum.Type != ArgNumber {
5618+
return fnNum
5619+
}
5620+
subFn, ok := map[int]func(argsList *list.List) formulaArg{
5621+
1: fn.AVERAGE, 101: fn.AVERAGE,
5622+
2: fn.COUNT, 102: fn.COUNT,
5623+
3: fn.COUNTA, 103: fn.COUNTA,
5624+
4: fn.MAX, 104: fn.MAX,
5625+
5: fn.MIN, 105: fn.MIN,
5626+
6: fn.PRODUCT, 106: fn.PRODUCT,
5627+
7: fn.STDEV, 107: fn.STDEV,
5628+
8: fn.STDEVP, 108: fn.STDEVP,
5629+
9: fn.SUM, 109: fn.SUM,
5630+
10: fn.VAR, 110: fn.VAR,
5631+
11: fn.VARP, 111: fn.VARP,
5632+
}[int(fnNum.Number)]
5633+
if !ok {
5634+
return newErrorFormulaArg(formulaErrorVALUE, "SUBTOTAL has invalid function_num")
5635+
}
5636+
subArgList := list.New().Init()
5637+
for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() {
5638+
subArgList.PushBack(arg.Value.(formulaArg))
5639+
}
5640+
return subFn(subArgList)
5641+
}
5642+
55585643
// SUM function adds together a supplied set of numbers and returns the sum of
55595644
// these values. The syntax of the function is:
55605645
//
@@ -11622,8 +11707,7 @@ func (fn *formulaFuncs) OR(argsList *list.List) formulaArg {
1162211707
}
1162311708
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
1162411709
case ArgNumber:
11625-
or = token.Number != 0
11626-
if or {
11710+
if or = token.Number != 0; or {
1162711711
return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(or)))
1162811712
}
1162911713
case ArgMatrix:

calc_test.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,16 +393,34 @@ func TestCalcCellValue(t *testing.T) {
393393
"=ACOSH(2.5)": "1.56679923697241",
394394
"=ACOSH(5)": "2.29243166956118",
395395
"=ACOSH(ACOSH(5))": "1.47138332153668",
396-
// ACOT
396+
// _xlfn.ACOT
397397
"=_xlfn.ACOT(1)": "0.785398163397448",
398398
"=_xlfn.ACOT(-2)": "2.67794504458899",
399399
"=_xlfn.ACOT(0)": "1.5707963267949",
400400
"=_xlfn.ACOT(_xlfn.ACOT(0))": "0.566911504941009",
401-
// ACOTH
401+
// _xlfn.ACOTH
402402
"=_xlfn.ACOTH(-5)": "-0.202732554054082",
403403
"=_xlfn.ACOTH(1.1)": "1.52226121886171",
404404
"=_xlfn.ACOTH(2)": "0.549306144334055",
405405
"=_xlfn.ACOTH(ABS(-2))": "0.549306144334055",
406+
// _xlfn.AGGREGATE
407+
"=_xlfn.AGGREGATE(1,0,A1:A6)": "1.5",
408+
"=_xlfn.AGGREGATE(2,0,A1:A6)": "4",
409+
"=_xlfn.AGGREGATE(3,0,A1:A6)": "4",
410+
"=_xlfn.AGGREGATE(4,0,A1:A6)": "3",
411+
"=_xlfn.AGGREGATE(5,0,A1:A6)": "0",
412+
"=_xlfn.AGGREGATE(6,0,A1:A6)": "0",
413+
"=_xlfn.AGGREGATE(7,0,A1:A6)": "1.29099444873581",
414+
"=_xlfn.AGGREGATE(8,0,A1:A6)": "1.11803398874989",
415+
"=_xlfn.AGGREGATE(9,0,A1:A6)": "6",
416+
"=_xlfn.AGGREGATE(10,0,A1:A6)": "1.66666666666667",
417+
"=_xlfn.AGGREGATE(11,0,A1:A6)": "1.25",
418+
"=_xlfn.AGGREGATE(12,0,A1:A6)": "1.5",
419+
"=_xlfn.AGGREGATE(14,0,A1:A6,1)": "3",
420+
"=_xlfn.AGGREGATE(15,0,A1:A6,1)": "0",
421+
"=_xlfn.AGGREGATE(16,0,A1:A6,1)": "3",
422+
"=_xlfn.AGGREGATE(17,0,A1:A6,1)": "0.75",
423+
"=_xlfn.AGGREGATE(19,0,A1:A6,1)": "0.25",
406424
// ARABIC
407425
"=_xlfn.ARABIC(\"IV\")": "4",
408426
"=_xlfn.ARABIC(\"-IV\")": "-4",
@@ -791,6 +809,31 @@ func TestCalcCellValue(t *testing.T) {
791809
// POISSON
792810
"=POISSON(20,25,FALSE)": "0.0519174686084913",
793811
"=POISSON(35,40,TRUE)": "0.242414197690103",
812+
// SUBTOTAL
813+
"=SUBTOTAL(1,A1:A6)": "1.5",
814+
"=SUBTOTAL(2,A1:A6)": "4",
815+
"=SUBTOTAL(3,A1:A6)": "4",
816+
"=SUBTOTAL(4,A1:A6)": "3",
817+
"=SUBTOTAL(5,A1:A6)": "0",
818+
"=SUBTOTAL(6,A1:A6)": "0",
819+
"=SUBTOTAL(7,A1:A6)": "1.29099444873581",
820+
"=SUBTOTAL(8,A1:A6)": "1.11803398874989",
821+
"=SUBTOTAL(9,A1:A6)": "6",
822+
"=SUBTOTAL(10,A1:A6)": "1.66666666666667",
823+
"=SUBTOTAL(11,A1:A6)": "1.25",
824+
"=SUBTOTAL(101,A1:A6)": "1.5",
825+
"=SUBTOTAL(102,A1:A6)": "4",
826+
"=SUBTOTAL(103,A1:A6)": "4",
827+
"=SUBTOTAL(104,A1:A6)": "3",
828+
"=SUBTOTAL(105,A1:A6)": "0",
829+
"=SUBTOTAL(106,A1:A6)": "0",
830+
"=SUBTOTAL(107,A1:A6)": "1.29099444873581",
831+
"=SUBTOTAL(108,A1:A6)": "1.11803398874989",
832+
"=SUBTOTAL(109,A1:A6)": "6",
833+
"=SUBTOTAL(109,A1:A6,A1:A6)": "12",
834+
"=SUBTOTAL(110,A1:A6)": "1.66666666666667",
835+
"=SUBTOTAL(111,A1:A6)": "1.25",
836+
"=SUBTOTAL(111,A1:A6,A1:A6)": "1.25",
794837
// SUM
795838
"=SUM(1,2)": "3",
796839
`=SUM("",1,2)`: "3",
@@ -2344,6 +2387,15 @@ func TestCalcCellValue(t *testing.T) {
23442387
"=_xlfn.ACOTH()": "ACOTH requires 1 numeric argument",
23452388
`=_xlfn.ACOTH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax",
23462389
"=_xlfn.ACOTH(_xlfn.ACOTH(2))": "#NUM!",
2390+
// _xlfn.AGGREGATE
2391+
"=_xlfn.AGGREGATE()": "AGGREGATE requires at least 3 arguments",
2392+
"=_xlfn.AGGREGATE(\"\",0,A4:A5)": "strconv.ParseFloat: parsing \"\": invalid syntax",
2393+
"=_xlfn.AGGREGATE(1,\"\",A4:A5)": "strconv.ParseFloat: parsing \"\": invalid syntax",
2394+
"=_xlfn.AGGREGATE(0,A4:A5)": "AGGREGATE has invalid function_num",
2395+
"=_xlfn.AGGREGATE(1,8,A4:A5)": "AGGREGATE has invalid options",
2396+
"=_xlfn.AGGREGATE(1,0,A5:A6)": "#DIV/0!",
2397+
"=_xlfn.AGGREGATE(13,0,A1:A6)": "#N/A",
2398+
"=_xlfn.AGGREGATE(18,0,A1:A6,1)": "#NUM!",
23472399
// _xlfn.ARABIC
23482400
"=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument",
23492401
"=_xlfn.ARABIC(\"" + strings.Repeat("I", 256) + "\")": "#VALUE!",
@@ -2611,6 +2663,11 @@ func TestCalcCellValue(t *testing.T) {
26112663
"=POISSON(0,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax",
26122664
"=POISSON(0,0,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax",
26132665
"=POISSON(0,-1,TRUE)": "#N/A",
2666+
// SUBTOTAL
2667+
"=SUBTOTAL()": "SUBTOTAL requires at least 2 arguments",
2668+
"=SUBTOTAL(\"\",A4:A5)": "strconv.ParseFloat: parsing \"\": invalid syntax",
2669+
"=SUBTOTAL(0,A4:A5)": "SUBTOTAL has invalid function_num",
2670+
"=SUBTOTAL(1,A5:A6)": "#DIV/0!",
26142671
// SUM
26152672
"=SUM((": ErrInvalidFormula.Error(),
26162673
"=SUM(-)": ErrInvalidFormula.Error(),

cell.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,7 @@ func getCellRichText(si *xlsxSI) (runs []RichTextRun) {
826826
if v.RPr.Color.Theme != nil {
827827
font.ColorTheme = v.RPr.Color.Theme
828828
}
829+
font.ColorIndexed = v.RPr.Color.Indexed
829830
font.ColorTint = v.RPr.Color.Tint
830831
}
831832
run.Font = &font

cell_test.go

Lines changed: 50 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -298,42 +298,46 @@ func TestGetCellValue(t *testing.T) {
298298
assert.NoError(t, err)
299299

300300
f.Sheet.Delete("xl/worksheets/sheet1.xml")
301-
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="1">
302-
<c r="A1"><v>2422.3000000000002</v></c>
303-
<c r="B1"><v>2422.3000000000002</v></c>
304-
<c r="C1"><v>12.4</v></c>
305-
<c r="D1"><v>964</v></c>
306-
<c r="E1"><v>1101.5999999999999</v></c>
307-
<c r="F1"><v>275.39999999999998</v></c>
308-
<c r="G1"><v>68.900000000000006</v></c>
309-
<c r="H1"><v>44385.208333333336</v></c>
310-
<c r="I1"><v>5.0999999999999996</v></c>
311-
<c r="J1"><v>5.1100000000000003</v></c>
312-
<c r="K1"><v>5.0999999999999996</v></c>
313-
<c r="L1"><v>5.1109999999999998</v></c>
314-
<c r="M1"><v>5.1111000000000004</v></c>
315-
<c r="N1"><v>2422.012345678</v></c>
316-
<c r="O1"><v>2422.0123456789</v></c>
317-
<c r="P1"><v>12.012345678901</v></c>
318-
<c r="Q1"><v>964</v></c>
319-
<c r="R1"><v>1101.5999999999999</v></c>
320-
<c r="S1"><v>275.39999999999998</v></c>
321-
<c r="T1"><v>68.900000000000006</v></c>
322-
<c r="U1"><v>8.8880000000000001E-2</v></c>
323-
<c r="V1"><v>4.0000000000000003e-5</v></c>
324-
<c r="W1"><v>2422.3000000000002</v></c>
325-
<c r="X1"><v>1101.5999999999999</v></c>
326-
<c r="Y1"><v>275.39999999999998</v></c>
327-
<c r="Z1"><v>68.900000000000006</v></c>
328-
<c r="AA1"><v>1.1000000000000001</v></c>
329-
<c r="AB1" t="str"><v>1234567890123_4</v></c>
330-
<c r="AC1" t="str"><v>123456789_0123_4</v></c>
331-
<c r="AD1"><v>+0.0000000000000000002399999999999992E-4</v></c>
332-
<c r="AE1"><v>7.2399999999999992E-2</v></c>
333-
</row>`)))
301+
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `
302+
<row r="1"><c r="A1"><v>2422.3000000000002</v></c></row>
303+
<row r="2"><c r="A2"><v>2422.3000000000002</v></c></row>
304+
<row r="3"><c r="A3"><v>12.4</v></c></row>
305+
<row r="4"><c r="A4"><v>964</v></c></row>
306+
<row r="5"><c r="A5"><v>1101.5999999999999</v></c></row>
307+
<row r="6"><c r="A6"><v>275.39999999999998</v></c></row>
308+
<row r="7"><c r="A7"><v>68.900000000000006</v></c></row>
309+
<row r="8"><c r="A8"><v>44385.208333333336</v></c></row>
310+
<row r="9"><c r="A9"><v>5.0999999999999996</v></c></row>
311+
<row r="10"><c r="A10"><v>5.1100000000000003</v></c></row>
312+
<row r="11"><c r="A11"><v>5.0999999999999996</v></c></row>
313+
<row r="12"><c r="A12"><v>5.1109999999999998</v></c></row>
314+
<row r="13"><c r="A13"><v>5.1111000000000004</v></c></row>
315+
<row r="14"><c r="A14"><v>2422.012345678</v></c></row>
316+
<row r="15"><c r="A15"><v>2422.0123456789</v></c></row>
317+
<row r="16"><c r="A16"><v>12.012345678901</v></c></row>
318+
<row r="17"><c r="A17"><v>964</v></c></row>
319+
<row r="18"><c r="A18"><v>1101.5999999999999</v></c></row>
320+
<row r="19"><c r="A19"><v>275.39999999999998</v></c></row>
321+
<row r="20"><c r="A20"><v>68.900000000000006</v></c></row>
322+
<row r="21"><c r="A21"><v>8.8880000000000001E-2</v></c></row>
323+
<row r="22"><c r="A22"><v>4.0000000000000003e-5</v></c></row>
324+
<row r="23"><c r="A23"><v>2422.3000000000002</v></c></row>
325+
<row r="24"><c r="A24"><v>1101.5999999999999</v></c></row>
326+
<row r="25"><c r="A25"><v>275.39999999999998</v></c></row>
327+
<row r="26"><c r="A26"><v>68.900000000000006</v></c></row>
328+
<row r="27"><c r="A27"><v>1.1000000000000001</v></c></row>
329+
<row r="28"><c r="A28" t="str"><v>1234567890123_4</v></c></row>
330+
<row r="29"><c r="A29" t="str"><v>123456789_0123_4</v></c></row>
331+
<row r="30"><c r="A30"><v>+0.0000000000000000002399999999999992E-4</v></c></row>
332+
<row r="31"><c r="A31"><v>7.2399999999999992E-2</v></c></row>
333+
<row r="32"><c r="A32" t="d"><v>20200208T080910.123</v></c></row>
334+
<row r="33"><c r="A33" t="d"><v>20200208T080910,123</v></c></row>
335+
<row r="34"><c r="A34" t="d"><v>20221022T150529Z</v></c></row>
336+
<row r="35"><c r="A35" t="d"><v>2022-10-22T15:05:29Z</v></c></row>
337+
<row r="36"><c r="A36" t="d"><v>2020-07-10 15:00:00.000</v></c></row>`)))
334338
f.checked = nil
335-
rows, err = f.GetRows("Sheet1")
336-
assert.Equal(t, [][]string{{
339+
rows, err = f.GetCols("Sheet1")
340+
assert.Equal(t, []string{
337341
"2422.3",
338342
"2422.3",
339343
"12.4",
@@ -365,7 +369,12 @@ func TestGetCellValue(t *testing.T) {
365369
"123456789_0123_4",
366370
"2.39999999999999E-23",
367371
"0.0724",
368-
}}, rows)
372+
"43869.3397004977",
373+
"43869.3397004977",
374+
"44856.6288078704",
375+
"44856.6288078704",
376+
"2020-07-10 15:00:00.000",
377+
}, rows[0])
369378
assert.NoError(t, err)
370379
}
371380

@@ -596,9 +605,10 @@ func TestSetCellRichText(t *testing.T) {
596605
{
597606
Text: "bold",
598607
Font: &Font{
599-
Bold: true,
600-
Color: "2354e8",
601-
Family: "Times New Roman",
608+
Bold: true,
609+
Color: "2354e8",
610+
ColorIndexed: 0,
611+
Family: "Times New Roman",
602612
},
603613
},
604614
{
@@ -742,7 +752,7 @@ func TestSharedStringsError(t *testing.T) {
742752
assert.Equal(t, "1", f.getFromStringItem(1))
743753
// Cleanup undelete temporary files
744754
assert.NoError(t, os.Remove(tempFile.(string)))
745-
// Test reload the file error on set cell cell and rich text. The error message was different between macOS and Windows.
755+
// Test reload the file error on set cell value and rich text. The error message was different between macOS and Windows.
746756
err = f.SetCellValue("Sheet1", "A19", "A19")
747757
assert.Error(t, err)
748758

file.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ func (f *File) writeToZip(zw *zip.Writer) error {
176176
f.workBookWriter()
177177
f.workSheetWriter()
178178
f.relsWriter()
179-
f.sharedStringsLoader()
179+
_ = f.sharedStringsLoader()
180180
f.sharedStringsWriter()
181181
f.styleSheetWriter()
182182

0 commit comments

Comments
 (0)