Skip to content

Commit

Permalink
Support get cell value which contains a date in the ISO 8601 format
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
xuri committed Oct 23, 2022
1 parent 2dc0e8c commit 426ecf0
Show file tree
Hide file tree
Showing 15 changed files with 294 additions and 116 deletions.
94 changes: 89 additions & 5 deletions calc.go
Expand Up @@ -339,6 +339,7 @@ type formulaFuncs struct {
// ACOT
// ACOTH
// ADDRESS
// AGGREGATE
// AMORDEGRC
// AMORLINC
// AND
Expand Down Expand Up @@ -700,6 +701,7 @@ type formulaFuncs struct {
// STDEVPA
// STEYX
// SUBSTITUTE
// SUBTOTAL
// SUM
// SUMIF
// SUMIFS
Expand Down Expand Up @@ -872,7 +874,6 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T
var err error
opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack()
var inArray, inArrayRow bool
var arrayRow []formulaArg
for i := 0; i < len(tokens); i++ {
token := tokens[i]

Expand Down Expand Up @@ -981,7 +982,6 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T
argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(token.TValue))
}
if inArrayRow && isOperand(token) {
arrayRow = append(arrayRow, tokenToFormulaArg(token))
continue
}
if inArrayRow && isFunctionStopToken(token) {
Expand All @@ -990,7 +990,7 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T
}
if inArray && isFunctionStopToken(token) {
argsStack.Peek().(*list.List).PushBack(opfdStack.Pop())
arrayRow, inArray = []formulaArg{}, false
inArray = false
continue
}
if err = f.evalInfixExpFunc(ctx, sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil {
Expand Down Expand Up @@ -3559,6 +3559,56 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) formulaArg {
return newNumberFormulaArg(math.Atanh(1 / arg.Number))
}

// AGGREGATE function returns the result of a specified operation or function,
// applied to a list or database of values. The syntax of the function is:
//
// AGGREGATE(function_num,options,ref1,[ref2],...)
func (fn *formulaFuncs) AGGREGATE(argsList *list.List) formulaArg {
if argsList.Len() < 2 {
return newErrorFormulaArg(formulaErrorVALUE, "AGGREGATE requires at least 3 arguments")
}
var fnNum, opts formulaArg
if fnNum = argsList.Front().Value.(formulaArg).ToNumber(); fnNum.Type != ArgNumber {
return fnNum
}
subFn, ok := map[int]func(argsList *list.List) formulaArg{
1: fn.AVERAGE,
2: fn.COUNT,
3: fn.COUNTA,
4: fn.MAX,
5: fn.MIN,
6: fn.PRODUCT,
7: fn.STDEVdotS,
8: fn.STDEVdotP,
9: fn.SUM,
10: fn.VARdotS,
11: fn.VARdotP,
12: fn.MEDIAN,
13: fn.MODEdotSNGL,
14: fn.LARGE,
15: fn.SMALL,
16: fn.PERCENTILEdotINC,
17: fn.QUARTILEdotINC,
18: fn.PERCENTILEdotEXC,
19: fn.QUARTILEdotEXC,
}[int(fnNum.Number)]
if !ok {
return newErrorFormulaArg(formulaErrorVALUE, "AGGREGATE has invalid function_num")
}
if opts = argsList.Front().Next().Value.(formulaArg).ToNumber(); opts.Type != ArgNumber {
return opts
}
// TODO: apply option argument values to be ignored during the calculation
if int(opts.Number) < 0 || int(opts.Number) > 7 {
return newErrorFormulaArg(formulaErrorVALUE, "AGGREGATE has invalid options")
}
subArgList := list.New().Init()
for arg := argsList.Front().Next().Next(); arg != nil; arg = arg.Next() {
subArgList.PushBack(arg.Value.(formulaArg))
}
return subFn(subArgList)
}

// ARABIC function converts a Roman numeral into an Arabic numeral. The syntax
// of the function is:
//
Expand Down Expand Up @@ -5555,6 +5605,41 @@ func (fn *formulaFuncs) POISSON(argsList *list.List) formulaArg {
return newNumberFormulaArg(math.Exp(0-mean.Number) * math.Pow(mean.Number, x.Number) / fact(x.Number))
}

// SUBTOTAL function performs a specified calculation (e.g. the sum, product,
// average, etc.) for a supplied set of values. The syntax of the function is:
//
// SUBTOTAL(function_num,ref1,[ref2],...)
func (fn *formulaFuncs) SUBTOTAL(argsList *list.List) formulaArg {
if argsList.Len() < 2 {
return newErrorFormulaArg(formulaErrorVALUE, "SUBTOTAL requires at least 2 arguments")
}
var fnNum formulaArg
if fnNum = argsList.Front().Value.(formulaArg).ToNumber(); fnNum.Type != ArgNumber {
return fnNum
}
subFn, ok := map[int]func(argsList *list.List) formulaArg{
1: fn.AVERAGE, 101: fn.AVERAGE,
2: fn.COUNT, 102: fn.COUNT,
3: fn.COUNTA, 103: fn.COUNTA,
4: fn.MAX, 104: fn.MAX,
5: fn.MIN, 105: fn.MIN,
6: fn.PRODUCT, 106: fn.PRODUCT,
7: fn.STDEV, 107: fn.STDEV,
8: fn.STDEVP, 108: fn.STDEVP,
9: fn.SUM, 109: fn.SUM,
10: fn.VAR, 110: fn.VAR,
11: fn.VARP, 111: fn.VARP,
}[int(fnNum.Number)]
if !ok {
return newErrorFormulaArg(formulaErrorVALUE, "SUBTOTAL has invalid function_num")
}
subArgList := list.New().Init()
for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() {
subArgList.PushBack(arg.Value.(formulaArg))
}
return subFn(subArgList)
}

// SUM function adds together a supplied set of numbers and returns the sum of
// these values. The syntax of the function is:
//
Expand Down Expand Up @@ -11622,8 +11707,7 @@ func (fn *formulaFuncs) OR(argsList *list.List) formulaArg {
}
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
case ArgNumber:
or = token.Number != 0
if or {
if or = token.Number != 0; or {
return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(or)))
}
case ArgMatrix:
Expand Down
61 changes: 59 additions & 2 deletions calc_test.go
Expand Up @@ -393,16 +393,34 @@ func TestCalcCellValue(t *testing.T) {
"=ACOSH(2.5)": "1.56679923697241",
"=ACOSH(5)": "2.29243166956118",
"=ACOSH(ACOSH(5))": "1.47138332153668",
// ACOT
// _xlfn.ACOT
"=_xlfn.ACOT(1)": "0.785398163397448",
"=_xlfn.ACOT(-2)": "2.67794504458899",
"=_xlfn.ACOT(0)": "1.5707963267949",
"=_xlfn.ACOT(_xlfn.ACOT(0))": "0.566911504941009",
// ACOTH
// _xlfn.ACOTH
"=_xlfn.ACOTH(-5)": "-0.202732554054082",
"=_xlfn.ACOTH(1.1)": "1.52226121886171",
"=_xlfn.ACOTH(2)": "0.549306144334055",
"=_xlfn.ACOTH(ABS(-2))": "0.549306144334055",
// _xlfn.AGGREGATE
"=_xlfn.AGGREGATE(1,0,A1:A6)": "1.5",
"=_xlfn.AGGREGATE(2,0,A1:A6)": "4",
"=_xlfn.AGGREGATE(3,0,A1:A6)": "4",
"=_xlfn.AGGREGATE(4,0,A1:A6)": "3",
"=_xlfn.AGGREGATE(5,0,A1:A6)": "0",
"=_xlfn.AGGREGATE(6,0,A1:A6)": "0",
"=_xlfn.AGGREGATE(7,0,A1:A6)": "1.29099444873581",
"=_xlfn.AGGREGATE(8,0,A1:A6)": "1.11803398874989",
"=_xlfn.AGGREGATE(9,0,A1:A6)": "6",
"=_xlfn.AGGREGATE(10,0,A1:A6)": "1.66666666666667",
"=_xlfn.AGGREGATE(11,0,A1:A6)": "1.25",
"=_xlfn.AGGREGATE(12,0,A1:A6)": "1.5",
"=_xlfn.AGGREGATE(14,0,A1:A6,1)": "3",
"=_xlfn.AGGREGATE(15,0,A1:A6,1)": "0",
"=_xlfn.AGGREGATE(16,0,A1:A6,1)": "3",
"=_xlfn.AGGREGATE(17,0,A1:A6,1)": "0.75",
"=_xlfn.AGGREGATE(19,0,A1:A6,1)": "0.25",
// ARABIC
"=_xlfn.ARABIC(\"IV\")": "4",
"=_xlfn.ARABIC(\"-IV\")": "-4",
Expand Down Expand Up @@ -791,6 +809,31 @@ func TestCalcCellValue(t *testing.T) {
// POISSON
"=POISSON(20,25,FALSE)": "0.0519174686084913",
"=POISSON(35,40,TRUE)": "0.242414197690103",
// SUBTOTAL
"=SUBTOTAL(1,A1:A6)": "1.5",
"=SUBTOTAL(2,A1:A6)": "4",
"=SUBTOTAL(3,A1:A6)": "4",
"=SUBTOTAL(4,A1:A6)": "3",
"=SUBTOTAL(5,A1:A6)": "0",
"=SUBTOTAL(6,A1:A6)": "0",
"=SUBTOTAL(7,A1:A6)": "1.29099444873581",
"=SUBTOTAL(8,A1:A6)": "1.11803398874989",
"=SUBTOTAL(9,A1:A6)": "6",
"=SUBTOTAL(10,A1:A6)": "1.66666666666667",
"=SUBTOTAL(11,A1:A6)": "1.25",
"=SUBTOTAL(101,A1:A6)": "1.5",
"=SUBTOTAL(102,A1:A6)": "4",
"=SUBTOTAL(103,A1:A6)": "4",
"=SUBTOTAL(104,A1:A6)": "3",
"=SUBTOTAL(105,A1:A6)": "0",
"=SUBTOTAL(106,A1:A6)": "0",
"=SUBTOTAL(107,A1:A6)": "1.29099444873581",
"=SUBTOTAL(108,A1:A6)": "1.11803398874989",
"=SUBTOTAL(109,A1:A6)": "6",
"=SUBTOTAL(109,A1:A6,A1:A6)": "12",
"=SUBTOTAL(110,A1:A6)": "1.66666666666667",
"=SUBTOTAL(111,A1:A6)": "1.25",
"=SUBTOTAL(111,A1:A6,A1:A6)": "1.25",
// SUM
"=SUM(1,2)": "3",
`=SUM("",1,2)`: "3",
Expand Down Expand Up @@ -2344,6 +2387,15 @@ func TestCalcCellValue(t *testing.T) {
"=_xlfn.ACOTH()": "ACOTH requires 1 numeric argument",
`=_xlfn.ACOTH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax",
"=_xlfn.ACOTH(_xlfn.ACOTH(2))": "#NUM!",
// _xlfn.AGGREGATE
"=_xlfn.AGGREGATE()": "AGGREGATE requires at least 3 arguments",
"=_xlfn.AGGREGATE(\"\",0,A4:A5)": "strconv.ParseFloat: parsing \"\": invalid syntax",
"=_xlfn.AGGREGATE(1,\"\",A4:A5)": "strconv.ParseFloat: parsing \"\": invalid syntax",
"=_xlfn.AGGREGATE(0,A4:A5)": "AGGREGATE has invalid function_num",
"=_xlfn.AGGREGATE(1,8,A4:A5)": "AGGREGATE has invalid options",
"=_xlfn.AGGREGATE(1,0,A5:A6)": "#DIV/0!",
"=_xlfn.AGGREGATE(13,0,A1:A6)": "#N/A",
"=_xlfn.AGGREGATE(18,0,A1:A6,1)": "#NUM!",
// _xlfn.ARABIC
"=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument",
"=_xlfn.ARABIC(\"" + strings.Repeat("I", 256) + "\")": "#VALUE!",
Expand Down Expand Up @@ -2611,6 +2663,11 @@ func TestCalcCellValue(t *testing.T) {
"=POISSON(0,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax",
"=POISSON(0,0,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax",
"=POISSON(0,-1,TRUE)": "#N/A",
// SUBTOTAL
"=SUBTOTAL()": "SUBTOTAL requires at least 2 arguments",
"=SUBTOTAL(\"\",A4:A5)": "strconv.ParseFloat: parsing \"\": invalid syntax",
"=SUBTOTAL(0,A4:A5)": "SUBTOTAL has invalid function_num",
"=SUBTOTAL(1,A5:A6)": "#DIV/0!",
// SUM
"=SUM((": ErrInvalidFormula.Error(),
"=SUM(-)": ErrInvalidFormula.Error(),
Expand Down
1 change: 1 addition & 0 deletions cell.go
Expand Up @@ -826,6 +826,7 @@ func getCellRichText(si *xlsxSI) (runs []RichTextRun) {
if v.RPr.Color.Theme != nil {
font.ColorTheme = v.RPr.Color.Theme
}
font.ColorIndexed = v.RPr.Color.Indexed
font.ColorTint = v.RPr.Color.Tint
}
run.Font = &font
Expand Down
90 changes: 50 additions & 40 deletions cell_test.go
Expand Up @@ -298,42 +298,46 @@ func TestGetCellValue(t *testing.T) {
assert.NoError(t, err)

f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="1">
<c r="A1"><v>2422.3000000000002</v></c>
<c r="B1"><v>2422.3000000000002</v></c>
<c r="C1"><v>12.4</v></c>
<c r="D1"><v>964</v></c>
<c r="E1"><v>1101.5999999999999</v></c>
<c r="F1"><v>275.39999999999998</v></c>
<c r="G1"><v>68.900000000000006</v></c>
<c r="H1"><v>44385.208333333336</v></c>
<c r="I1"><v>5.0999999999999996</v></c>
<c r="J1"><v>5.1100000000000003</v></c>
<c r="K1"><v>5.0999999999999996</v></c>
<c r="L1"><v>5.1109999999999998</v></c>
<c r="M1"><v>5.1111000000000004</v></c>
<c r="N1"><v>2422.012345678</v></c>
<c r="O1"><v>2422.0123456789</v></c>
<c r="P1"><v>12.012345678901</v></c>
<c r="Q1"><v>964</v></c>
<c r="R1"><v>1101.5999999999999</v></c>
<c r="S1"><v>275.39999999999998</v></c>
<c r="T1"><v>68.900000000000006</v></c>
<c r="U1"><v>8.8880000000000001E-2</v></c>
<c r="V1"><v>4.0000000000000003e-5</v></c>
<c r="W1"><v>2422.3000000000002</v></c>
<c r="X1"><v>1101.5999999999999</v></c>
<c r="Y1"><v>275.39999999999998</v></c>
<c r="Z1"><v>68.900000000000006</v></c>
<c r="AA1"><v>1.1000000000000001</v></c>
<c r="AB1" t="str"><v>1234567890123_4</v></c>
<c r="AC1" t="str"><v>123456789_0123_4</v></c>
<c r="AD1"><v>+0.0000000000000000002399999999999992E-4</v></c>
<c r="AE1"><v>7.2399999999999992E-2</v></c>
</row>`)))
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `
<row r="1"><c r="A1"><v>2422.3000000000002</v></c></row>
<row r="2"><c r="A2"><v>2422.3000000000002</v></c></row>
<row r="3"><c r="A3"><v>12.4</v></c></row>
<row r="4"><c r="A4"><v>964</v></c></row>
<row r="5"><c r="A5"><v>1101.5999999999999</v></c></row>
<row r="6"><c r="A6"><v>275.39999999999998</v></c></row>
<row r="7"><c r="A7"><v>68.900000000000006</v></c></row>
<row r="8"><c r="A8"><v>44385.208333333336</v></c></row>
<row r="9"><c r="A9"><v>5.0999999999999996</v></c></row>
<row r="10"><c r="A10"><v>5.1100000000000003</v></c></row>
<row r="11"><c r="A11"><v>5.0999999999999996</v></c></row>
<row r="12"><c r="A12"><v>5.1109999999999998</v></c></row>
<row r="13"><c r="A13"><v>5.1111000000000004</v></c></row>
<row r="14"><c r="A14"><v>2422.012345678</v></c></row>
<row r="15"><c r="A15"><v>2422.0123456789</v></c></row>
<row r="16"><c r="A16"><v>12.012345678901</v></c></row>
<row r="17"><c r="A17"><v>964</v></c></row>
<row r="18"><c r="A18"><v>1101.5999999999999</v></c></row>
<row r="19"><c r="A19"><v>275.39999999999998</v></c></row>
<row r="20"><c r="A20"><v>68.900000000000006</v></c></row>
<row r="21"><c r="A21"><v>8.8880000000000001E-2</v></c></row>
<row r="22"><c r="A22"><v>4.0000000000000003e-5</v></c></row>
<row r="23"><c r="A23"><v>2422.3000000000002</v></c></row>
<row r="24"><c r="A24"><v>1101.5999999999999</v></c></row>
<row r="25"><c r="A25"><v>275.39999999999998</v></c></row>
<row r="26"><c r="A26"><v>68.900000000000006</v></c></row>
<row r="27"><c r="A27"><v>1.1000000000000001</v></c></row>
<row r="28"><c r="A28" t="str"><v>1234567890123_4</v></c></row>
<row r="29"><c r="A29" t="str"><v>123456789_0123_4</v></c></row>
<row r="30"><c r="A30"><v>+0.0000000000000000002399999999999992E-4</v></c></row>
<row r="31"><c r="A31"><v>7.2399999999999992E-2</v></c></row>
<row r="32"><c r="A32" t="d"><v>20200208T080910.123</v></c></row>
<row r="33"><c r="A33" t="d"><v>20200208T080910,123</v></c></row>
<row r="34"><c r="A34" t="d"><v>20221022T150529Z</v></c></row>
<row r="35"><c r="A35" t="d"><v>2022-10-22T15:05:29Z</v></c></row>
<row r="36"><c r="A36" t="d"><v>2020-07-10 15:00:00.000</v></c></row>`)))
f.checked = nil
rows, err = f.GetRows("Sheet1")
assert.Equal(t, [][]string{{
rows, err = f.GetCols("Sheet1")
assert.Equal(t, []string{
"2422.3",
"2422.3",
"12.4",
Expand Down Expand Up @@ -365,7 +369,12 @@ func TestGetCellValue(t *testing.T) {
"123456789_0123_4",
"2.39999999999999E-23",
"0.0724",
}}, rows)
"43869.3397004977",
"43869.3397004977",
"44856.6288078704",
"44856.6288078704",
"2020-07-10 15:00:00.000",
}, rows[0])
assert.NoError(t, err)
}

Expand Down Expand Up @@ -596,9 +605,10 @@ func TestSetCellRichText(t *testing.T) {
{
Text: "bold",
Font: &Font{
Bold: true,
Color: "2354e8",
Family: "Times New Roman",
Bold: true,
Color: "2354e8",
ColorIndexed: 0,
Family: "Times New Roman",
},
},
{
Expand Down Expand Up @@ -742,7 +752,7 @@ func TestSharedStringsError(t *testing.T) {
assert.Equal(t, "1", f.getFromStringItem(1))
// Cleanup undelete temporary files
assert.NoError(t, os.Remove(tempFile.(string)))
// Test reload the file error on set cell cell and rich text. The error message was different between macOS and Windows.
// Test reload the file error on set cell value and rich text. The error message was different between macOS and Windows.
err = f.SetCellValue("Sheet1", "A19", "A19")
assert.Error(t, err)

Expand Down
2 changes: 1 addition & 1 deletion file.go
Expand Up @@ -176,7 +176,7 @@ func (f *File) writeToZip(zw *zip.Writer) error {
f.workBookWriter()
f.workSheetWriter()
f.relsWriter()
f.sharedStringsLoader()
_ = f.sharedStringsLoader()
f.sharedStringsWriter()
f.styleSheetWriter()

Expand Down

0 comments on commit 426ecf0

Please sign in to comment.