Allow access to more formula attributes in SetCellFormula (#484)

* Allow access to more formula attributes in SetCellFormula

Make SetCellFormula variadic to not break API.
The new arguments are option arguments in which the type of
the formula and the ref attribute may be set.
These need to be set for an array formula to work.

* Add TestWriteArrayFormula to test optional parameters of SetCellFormula

TestWriteArrayFormula writes a document to the test directory that
contains array formulas that are used to calculate standard deviations.
The file also contains values calculated by the Go testcase, so the
results can be verified. It should be tested, if the array formula
works (i.e. shows a number, not an error) and that the values calculated
by the formula and those calculated by Go are the same.
This commit is contained in:
Christian Fiedler 2019-09-22 14:52:01 +02:00 committed by xuri
parent 3c636da460
commit 3280e1b686
2 changed files with 125 additions and 1 deletions

19
cell.go
View File

@ -273,9 +273,15 @@ func (f *File) GetCellFormula(sheet, axis string) (string, error) {
})
}
// FormulaOpts can be passed to SetCellFormula to use other formula types.
type FormulaOpts struct {
Type *string // Formula type
Ref *string // Shared formula ref
}
// SetCellFormula provides a function to set cell formula by given string and
// worksheet name.
func (f *File) SetCellFormula(sheet, axis, formula string) error {
func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) error {
xlsx, err := f.workSheetReader(sheet)
if err != nil {
return err
@ -295,6 +301,17 @@ func (f *File) SetCellFormula(sheet, axis, formula string) error {
} else {
cellData.F = &xlsxF{Content: formula}
}
for _, o := range opts {
if o.Type != nil {
cellData.F.T = *o.Type
}
if o.Ref != nil {
cellData.F.Ref = *o.Ref
}
}
return err
}

View File

@ -8,6 +8,7 @@ import (
_ "image/jpeg"
_ "image/png"
"io/ioutil"
"math"
"os"
"path/filepath"
"strconv"
@ -397,6 +398,112 @@ func TestMergeCell(t *testing.T) {
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx")))
}
// TestWriteArrayFormula tests the extended options of SetCellFormula by writing an array function
// to a workbook. In the resulting file, the lines 2 and 3 as well as 4 and 5 should have matching
// contents.
func TestWriteArrayFormula(t *testing.T) {
cell := func(col, row int) string {
c, err := CoordinatesToCellName(col, row)
if err != nil {
t.Fatal(err)
}
return c
}
f := NewFile()
sample := []string{"Sample 1", "Sample 2", "Sample 3"}
values := []int{1855, 1709, 1462, 1115, 1524, 625, 773, 126, 1027, 1696, 1078, 1917, 1109, 1753, 1884, 659, 994, 1911, 1925, 899, 196, 244, 1488, 1056, 1986, 66, 784, 725, 767, 1722, 1541, 1026, 1455, 264, 1538, 877, 1581, 1098, 383, 762, 237, 493, 29, 1923, 474, 430, 585, 688, 308, 200, 1259, 622, 798, 1048, 996, 601, 582, 332, 377, 805, 250, 1860, 1360, 840, 911, 1346, 1651, 1651, 665, 584, 1057, 1145, 925, 1752, 202, 149, 1917, 1398, 1894, 818, 714, 624, 1085, 1566, 635, 78, 313, 1686, 1820, 494, 614, 1913, 271, 1016, 338, 1301, 489, 1733, 1483, 1141}
assoc := []int{2, 0, 0, 0, 0, 1, 1, 0, 0, 1, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 1, 0, 2, 0, 2, 1, 2, 2, 2, 1, 0, 1, 0, 1, 1, 2, 0, 2, 1, 0, 2, 1, 0, 1, 0, 0, 2, 0, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 1, 1, 0, 1, 0, 2, 0, 0, 1, 2, 1, 0, 1, 0, 0, 2, 1, 1, 2, 0, 2, 1, 0, 2, 2, 2, 1, 0, 0, 1, 1, 1, 2, 0, 2, 0, 1, 1}
if len(values) != len(assoc) {
t.Fatal("values and assoc must be of same length")
}
// Average calculates the average of the n-th sample (0 <= n < len(sample)).
average := func(n int) int {
sum := 0
count := 0
for i := 0; i != len(values); i++ {
if assoc[i] == n {
sum += values[i]
count++
}
}
return int(math.Round(float64(sum) / float64(count)))
}
// Stdev calculates the standard deviation of the n-th sample (0 <= n < len(sample)).
stdev := func(n int) int {
avg := average(n)
sum := 0
count := 0
for i := 0; i != len(values); i++ {
if assoc[i] == n {
sum += (values[i] - avg) * (values[i] - avg)
count++
}
}
return int(math.Round(math.Sqrt(float64(sum) / float64(count))))
}
// Line 2 contains the results of AVERAGEIF
f.SetCellStr("Sheet1", "A2", "Average")
// Line 3 contains the average that was calculated in Go
f.SetCellStr("Sheet1", "A3", "Average (calculated)")
// Line 4 contains the results of the array function that calculates the standard deviation
f.SetCellStr("Sheet1", "A4", "Std. deviation")
// Line 5 contains the standard deviations calculated in Go
f.SetCellStr("Sheet1", "A5", "Std. deviation (calculated)")
f.SetCellStr("Sheet1", "B1", sample[0])
f.SetCellStr("Sheet1", "C1", sample[1])
f.SetCellStr("Sheet1", "D1", sample[2])
firstResLine := 8
f.SetCellStr("Sheet1", cell(1, firstResLine-1), "Result Values")
f.SetCellStr("Sheet1", cell(2, firstResLine-1), "Sample")
for i := 0; i != len(values); i++ {
valCell := cell(1, i+firstResLine)
assocCell := cell(2, i+firstResLine)
f.SetCellInt("Sheet1", valCell, values[i])
f.SetCellStr("Sheet1", assocCell, sample[assoc[i]])
}
valRange := fmt.Sprintf("$A$%d:$A$%d", firstResLine, len(values)+firstResLine-1)
assocRange := fmt.Sprintf("$B$%d:$B$%d", firstResLine, len(values)+firstResLine-1)
for i := 0; i != len(sample); i++ {
nameCell := cell(i+2, 1)
avgCell := cell(i+2, 2)
calcAvgCell := cell(i+2, 3)
stdevCell := cell(i+2, 4)
calcStdevCell := cell(i+2, 5)
f.SetCellInt("Sheet1", calcAvgCell, average(i))
f.SetCellInt("Sheet1", calcStdevCell, stdev(i))
// Average can be done with AVERAGEIF
f.SetCellFormula("Sheet1", avgCell, fmt.Sprintf("ROUND(AVERAGEIF(%s,%s,%s),0)", assocRange, nameCell, valRange))
ref := stdevCell + ":" + stdevCell
t := STCellFormulaTypeArray
// Use an array formula for standard deviation
f.SetCellFormula("Sheet1", stdevCell, fmt.Sprintf("ROUND(STDEVP(IF(%s=%s,%s)),0)", assocRange, nameCell, valRange),
FormulaOpts{}, FormulaOpts{Type: &t}, FormulaOpts{Ref: &ref})
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestWriteArrayFormula.xlsx")))
}
func TestSetCellStyleAlignment(t *testing.T) {
f, err := prepareTestBook1()
if !assert.NoError(t, err) {