diff --git a/calc.go b/calc.go index d9bf653c..1d4e96e1 100644 --- a/calc.go +++ b/calc.go @@ -315,9 +315,10 @@ type formulaFuncs struct { sheet, cell string } -// CalcCellValue provides a function to get calculated cell value. This -// feature is currently in working processing. Array formula, table formula -// and some other formulas are not supported currently. +// CalcCellValue provides a function to get calculated cell value. This feature +// is currently in working processing. Iterative calculation, implicit +// intersection, explicit intersection, array formula, table formula and some +// other formulas are not supported currently. // // Supported formula functions: // @@ -421,6 +422,8 @@ type formulaFuncs struct { // DAYS // DAYS360 // DB +// DCOUNT +// DCOUNTA // DDB // DEC2BIN // DEC2HEX @@ -488,6 +491,7 @@ type formulaFuncs struct { // HEX2OCT // HLOOKUP // HOUR +// HYPERLINK // HYPGEOM.DIST // HYPGEOMDIST // IF @@ -1602,12 +1606,18 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er var value, expected float64 var e error prepareValue := func(val, cond string) (value float64, expected float64, err error) { + percential := 1.0 + if strings.HasSuffix(cond, "%") { + cond = strings.TrimSuffix(cond, "%") + percential /= 100 + } if value, err = strconv.ParseFloat(val, 64); err != nil { return } - if expected, err = strconv.ParseFloat(criteria.Condition, 64); err != nil { + if expected, err = strconv.ParseFloat(cond, 64); err != nil { return } + expected *= percential return } switch criteria.Type { @@ -17957,3 +17967,155 @@ func (fn *formulaFuncs) YIELDMAT(argsList *list.List) formulaArg { result /= dsm.Number return newNumberFormulaArg(result) } + +// Database Functions + +// calcDatabase defines the structure for formula database. +type calcDatabase struct { + col, row int + indexMap map[int]int + database [][]formulaArg + criteria [][]formulaArg +} + +// newCalcDatabase function returns formula database by given data range of +// cells containing the database, field and criteria range. +func newCalcDatabase(database, field, criteria formulaArg) *calcDatabase { + db := calcDatabase{ + indexMap: make(map[int]int), + database: database.Matrix, + criteria: criteria.Matrix, + } + exp := len(database.Matrix) < 2 || len(database.Matrix[0]) < 1 || + len(criteria.Matrix) < 2 || len(criteria.Matrix[0]) < 1 + if field.Type != ArgEmpty { + if db.col = db.columnIndex(database.Matrix, field); exp || db.col < 0 || len(db.database[0]) <= db.col { + return nil + } + return &db + } + if db.col = -1; exp { + return nil + } + return &db +} + +// columnIndex return index by specifies column field within the database for +// which user want to return the count of non-blank cells. +func (db *calcDatabase) columnIndex(database [][]formulaArg, field formulaArg) int { + num := field.ToNumber() + if num.Type != ArgNumber && len(database) > 0 { + for i := 0; i < len(database[0]); i++ { + if title := database[0][i]; strings.EqualFold(title.Value(), field.Value()) { + return i + } + } + return -1 + } + return int(num.Number - 1) +} + +// criteriaEval evaluate formula criteria expression. +func (db *calcDatabase) criteriaEval() bool { + var ( + columns, rows = len(db.criteria[0]), len(db.criteria) + criteria = db.criteria + k int + matched bool + ) + if len(db.indexMap) == 0 { + fields := criteria[0] + for j := 0; j < columns; j++ { + if k = db.columnIndex(db.database, fields[j]); k < 0 { + return false + } + db.indexMap[j] = k + } + } + for i := 1; !matched && i < rows; i++ { + matched = true + for j := 0; matched && j < columns; j++ { + criteriaExp := db.criteria[i][j].Value() + if criteriaExp == "" { + continue + } + criteria := formulaCriteriaParser(criteriaExp) + cell := db.database[db.row][db.indexMap[j]].Value() + matched, _ = formulaCriteriaEval(cell, criteria) + } + } + return matched +} + +// value returns the current cell value. +func (db *calcDatabase) value() formulaArg { + if db.col == -1 { + return db.database[db.row][len(db.database[db.row])-1] + } + return db.database[db.row][db.col] +} + +// next will return true if find the matched cell in the database. +func (db *calcDatabase) next() bool { + matched, rows := false, len(db.database) + for !matched && db.row < rows { + if db.row++; db.row < rows { + matched = db.criteriaEval() + } + } + return matched +} + +// dcount is an implementation of the formula functions DCOUNT and DCOUNTA. +func (fn *formulaFuncs) dcount(name string, argsList *list.List) formulaArg { + if argsList.Len() < 2 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 2 arguments", name)) + } + if argsList.Len() > 3 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 3 arguments", name)) + } + field := newEmptyFormulaArg() + criteria := argsList.Back().Value.(formulaArg) + if argsList.Len() > 2 { + field = argsList.Front().Next().Value.(formulaArg) + } + var count float64 + database := argsList.Front().Value.(formulaArg) + db := newCalcDatabase(database, field, criteria) + if db == nil { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + for db.next() { + cell := db.value() + if cell.Value() == "" { + continue + } + if num := cell.ToNumber(); name == "DCOUNT" && num.Type != ArgNumber { + continue + } + count++ + } + return newNumberFormulaArg(count) +} + +// DOUNT function returns the number of cells containing numeric values, in a +// field (column) of a database for selected records only. The records to be +// included in the count are those that satisfy a set of one or more +// user-specified criteria. The syntax of the function is: +// +// DCOUNT(database,[field],criteria) +// +func (fn *formulaFuncs) DCOUNT(argsList *list.List) formulaArg { + return fn.dcount("DCOUNT", argsList) +} + +// DCOUNTA function returns the number of non-blank cells, in a field +// (column) of a database for selected records only. The records to be +// included in the count are those that satisfy a set of one or more +// user-specified criteria. The syntax of the function is: +// +// DCOUNTA(database,[field],criteria) +// +func (fn *formulaFuncs) DCOUNTA(argsList *list.List) formulaArg { + return fn.dcount("DCOUNTA", argsList) +} diff --git a/calc_test.go b/calc_test.go index b3eb1961..7cf9e487 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4603,6 +4603,63 @@ func TestCalcCOVAR(t *testing.T) { } } +func TestCalcDCOUNTandDCOUNTA(t *testing.T) { + cellData := [][]interface{}{ + {"Tree", "Height", "Age", "Yield", "Profit", "Height"}, + {"=Apple", ">1000%", nil, nil, nil, "<16"}, + {"=Pear"}, + {"Tree", "Height", "Age", "Yield", "Profit"}, + {"Apple", 18, 20, 14, 105}, + {"Pear", 12, 12, 10, 96}, + {"Cherry", 13, 14, 9, 105}, + {"Apple", 14, nil, 10, 75}, + {"Pear", 9, 8, 8, 77}, + {"Apple", 12, 11, 6, 45}, + } + f := prepareCalcData(cellData) + assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=\"=Apple\"")) + assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "=\"=Pear\"")) + assert.NoError(t, f.SetCellFormula("Sheet1", "C8", "=NA()")) + formulaList := map[string]string{ + "=DCOUNT(A4:E10,\"Age\",A1:F2)": "1", + "=DCOUNT(A4:E10,,A1:F2)": "2", + "=DCOUNT(A4:E10,\"Profit\",A1:F2)": "2", + "=DCOUNT(A4:E10,\"Tree\",A1:F2)": "0", + "=DCOUNT(A4:E10,\"Age\",A2:F3)": "0", + "=DCOUNTA(A4:E10,\"Age\",A1:F2)": "1", + "=DCOUNTA(A4:E10,,A1:F2)": "2", + "=DCOUNTA(A4:E10,\"Profit\",A1:F2)": "2", + "=DCOUNTA(A4:E10,\"Tree\",A1:F2)": "2", + "=DCOUNTA(A4:E10,\"Age\",A2:F3)": "0", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "A11", formula)) + result, err := f.CalcCellValue("Sheet1", "A11") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError := map[string]string{ + "=DCOUNT()": "DCOUNT requires at least 2 arguments", + "=DCOUNT(A4:E10,\"Age\",A1:F2,\"\")": "DCOUNT allows at most 3 arguments", + "=DCOUNT(A4,\"Age\",A1:F2)": "#VALUE!", + "=DCOUNT(A4:E10,NA(),A1:F2)": "#VALUE!", + "=DCOUNT(A4:E4,,A1:F2)": "#VALUE!", + "=DCOUNT(A4:E10,\"x\",A2:F3)": "#VALUE!", + "=DCOUNTA()": "DCOUNTA requires at least 2 arguments", + "=DCOUNTA(A4:E10,\"Age\",A1:F2,\"\")": "DCOUNTA allows at most 3 arguments", + "=DCOUNTA(A4,\"Age\",A1:F2)": "#VALUE!", + "=DCOUNTA(A4:E10,NA(),A1:F2)": "#VALUE!", + "=DCOUNTA(A4:E4,,A1:F2)": "#VALUE!", + "=DCOUNTA(A4:E10,\"x\",A2:F3)": "#VALUE!", + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "A11", formula)) + result, err := f.CalcCellValue("Sheet1", "A11") + assert.EqualError(t, err, expected, formula) + assert.Equal(t, "", result, formula) + } +} + func TestCalcFORMULATEXT(t *testing.T) { f, formulaText := NewFile(), "=SUM(B1:C1)" assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formulaText)) diff --git a/crypt.go b/crypt.go index 239208db..b00ccdf7 100644 --- a/crypt.go +++ b/crypt.go @@ -1226,10 +1226,10 @@ func (c *cfb) Writer(encryptionInfoBuffer, encryptedPackage []byte) []byte { } MSAT = c.writeMSAT(MSATBlocks, SATBlocks, MSAT) blocks, SAT := c.writeSAT(MSATBlocks, SATBlocks, SSATBlocks, directoryBlocks, fileBlocks, streamBlocks, SAT) - storage.writeUint32(0xE011CFD0) - storage.writeUint32(0xE11AB1A1) - storage.writeUint64(0x00) - storage.writeUint64(0x00) + for i := 0; i < 8; i++ { + storage.writeBytes([]byte{oleIdentifier[i]}) + } + storage.writeBytes(make([]byte, 16)) storage.writeUint16(0x003E) storage.writeUint16(0x0003) storage.writeUint16(-2) diff --git a/go.mod b/go.mod index b08e3d20..4d628fcf 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,11 @@ require ( github.com/richardlehane/mscfb v1.0.4 github.com/richardlehane/msoleps v1.0.3 // indirect github.com/stretchr/testify v1.7.1 - github.com/xuri/efp v0.0.0-20220407160117-ad0f7a785be8 + github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 - golang.org/x/net v0.0.0-20220524220425-1d687d428aca + golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index db6f6ad1..3ffe3394 100644 --- a/go.sum +++ b/go.sum @@ -13,21 +13,21 @@ github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTK github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/xuri/efp v0.0.0-20220407160117-ad0f7a785be8 h1:3X7aE0iLKJ5j+tz58BpvIZkXNV7Yq4jC93Z/rbN2Fxk= -github.com/xuri/efp v0.0.0-20220407160117-ad0f7a785be8/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c= +github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M= github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE= golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8= -golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=