From e1d660dda7c02475a8529a252e6c1fd171065429 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 12 Mar 2022 00:45:27 +0800 Subject: [PATCH 001/213] ref #65, new formula functions: GAMMA.INV and GAMMAINV and format code --- calc.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 24 ++++++++++++++++++ drawing.go | 2 +- 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/calc.go b/calc.go index 1f1408e751..e64d260301 100644 --- a/calc.go +++ b/calc.go @@ -432,6 +432,8 @@ type formulaFuncs struct { // GAMMA // GAMMA.DIST // GAMMADIST +// GAMMA.INV +// GAMMAINV // GAMMALN // GAUSS // GCD @@ -6088,6 +6090,75 @@ func (fn *formulaFuncs) GAMMADIST(argsList *list.List) formulaArg { return newNumberFormulaArg((1 / (math.Pow(beta.Number, alpha.Number) * math.Gamma(alpha.Number))) * math.Pow(x.Number, (alpha.Number-1)) * math.Exp(0-(x.Number/beta.Number))) } +// gammainv returns the inverse of the Gamma distribution for the specified +// value. +func gammainv(probability, alpha, beta float64) float64 { + xLo, xHi := 0.0, alpha*beta*5 + dx, x, xNew, result := 1024.0, 1.0, 1.0, 0.0 + for i := 0; math.Abs(dx) > 8.88e-016 && i <= 256; i++ { + result = incompleteGamma(alpha, x/beta) / math.Gamma(alpha) + error := result - probability + if error == 0 { + dx = 0 + } else if error < 0 { + xLo = x + } else { + xHi = x + } + pdf := (1 / (math.Pow(beta, alpha) * math.Gamma(alpha))) * math.Pow(x, (alpha-1)) * math.Exp(0-(x/beta)) + if pdf != 0 { + dx = error / pdf + xNew = x - dx + } + if xNew < xLo || xNew > xHi || pdf == 0 { + xNew = (xLo + xHi) / 2 + dx = xNew - x + } + x = xNew + } + return x +} + +// GAMMAdotINV function returns the inverse of the Gamma Cumulative +// Distribution. The syntax of the function is: +// +// GAMMA.INV(probability,alpha,beta) +// +func (fn *formulaFuncs) GAMMAdotINV(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "GAMMA.INV requires 3 arguments") + } + return fn.GAMMAINV(argsList) +} + +// GAMMAINV function returns the inverse of the Gamma Cumulative Distribution. +// The syntax of the function is: +// +// GAMMAINV(probability,alpha,beta) +// +func (fn *formulaFuncs) GAMMAINV(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "GAMMAINV requires 3 arguments") + } + var probability, alpha, beta formulaArg + if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { + return probability + } + if probability.Number < 0 || probability.Number >= 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if alpha = argsList.Front().Next().Value.(formulaArg).ToNumber(); alpha.Type != ArgNumber { + return alpha + } + if beta = argsList.Back().Value.(formulaArg).ToNumber(); beta.Type != ArgNumber { + return beta + } + if alpha.Number <= 0 || beta.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(gammainv(probability.Number, alpha.Number, beta.Number)) +} + // GAMMALN function returns the natural logarithm of the Gamma Function, Γ // (n). The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 0c9fb2ea0a..64076856f5 100644 --- a/calc_test.go +++ b/calc_test.go @@ -841,6 +841,12 @@ func TestCalcCellValue(t *testing.T) { // GAMMADIST "=GAMMADIST(6,3,2,FALSE)": "0.112020903827694", "=GAMMADIST(6,3,2,TRUE)": "0.576809918873156", + // GAMMA.INV + "=GAMMA.INV(0.5,3,2)": "5.348120627447122", + "=GAMMA.INV(0.5,0.5,1)": "0.227468211559786", + // GAMMAINV + "=GAMMAINV(0.5,3,2)": "5.348120627447122", + "=GAMMAINV(0.5,0.5,1)": "0.227468211559786", // GAMMALN "=GAMMALN(4.5)": "2.45373657084244", "=GAMMALN(INT(1))": "0", @@ -2406,6 +2412,24 @@ func TestCalcCellValue(t *testing.T) { "=GAMMADIST(-1,3,2,FALSE)": "#NUM!", "=GAMMADIST(6,0,2,FALSE)": "#NUM!", "=GAMMADIST(6,3,0,FALSE)": "#NUM!", + // GAMMA.INV + "=GAMMA.INV()": "GAMMA.INV requires 3 arguments", + "=GAMMA.INV(\"\",3,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=GAMMA.INV(0.5,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=GAMMA.INV(0.5,3,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=GAMMA.INV(-1,3,2)": "#NUM!", + "=GAMMA.INV(2,3,2)": "#NUM!", + "=GAMMA.INV(0.5,0,2)": "#NUM!", + "=GAMMA.INV(0.5,3,0)": "#NUM!", + // GAMMAINV + "=GAMMAINV()": "GAMMAINV requires 3 arguments", + "=GAMMAINV(\"\",3,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=GAMMAINV(0.5,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=GAMMAINV(0.5,3,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=GAMMAINV(-1,3,2)": "#NUM!", + "=GAMMAINV(2,3,2)": "#NUM!", + "=GAMMAINV(0.5,0,2)": "#NUM!", + "=GAMMAINV(0.5,3,0)": "#NUM!", // GAMMALN "=GAMMALN()": "GAMMALN requires 1 numeric argument", "=GAMMALN(F1)": "GAMMALN requires 1 numeric argument", diff --git a/drawing.go b/drawing.go index 3af789f58d..e3e7fa87c4 100644 --- a/drawing.go +++ b/drawing.go @@ -514,7 +514,7 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { // doughnut chart by given format sets. func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea { holeSize := 75 - if formatSet.HoleSize > 0 && formatSet.HoleSize <= 90{ + if formatSet.HoleSize > 0 && formatSet.HoleSize <= 90 { holeSize = formatSet.HoleSize } From 4220bf4327a35a07ac6b47b652a120ed978a3a2a Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 15 Mar 2022 00:05:02 +0800 Subject: [PATCH 002/213] ref #65, new formula functions: LOGNORM.INV and LOGINV * Update docs for the function `SetCellHyperLink` --- calc.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 20 ++++++++++++++++++++ cell.go | 7 +++++-- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index e64d260301..951b843776 100644 --- a/calc.go +++ b/calc.go @@ -505,6 +505,8 @@ type formulaFuncs struct { // LN // LOG // LOG10 +// LOGINV +// LOGNORM.INV // LOOKUP // LOWER // MATCH @@ -6424,6 +6426,53 @@ func (fn *formulaFuncs) FINV(argsList *list.List) formulaArg { return newNumberFormulaArg((1/calcBetainv(1-(1-probability.Number), d2.Number/2, d1.Number/2, 0, 1) - 1) * (d2.Number / d1.Number)) } +// LOGINV function calculates the inverse of the Cumulative Log-Normal +// Distribution Function of x, for a supplied probability. The syntax of the +// function is: +// +// LOGINV(probability,mean,standard_dev) +// +func (fn *formulaFuncs) LOGINV(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "LOGINV requires 3 arguments") + } + var probability, mean, stdDev formulaArg + if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { + return probability + } + if mean = argsList.Front().Next().Value.(formulaArg).ToNumber(); mean.Type != ArgNumber { + return mean + } + if stdDev = argsList.Back().Value.(formulaArg).ToNumber(); stdDev.Type != ArgNumber { + return stdDev + } + if probability.Number <= 0 || probability.Number >= 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if stdDev.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + args := list.New() + args.PushBack(probability) + args.PushBack(newNumberFormulaArg(0)) + args.PushBack(newNumberFormulaArg(1)) + norminv := fn.NORMINV(args) + return newNumberFormulaArg(math.Exp(mean.Number + stdDev.Number*norminv.Number)) +} + +// LOGNORMdotINV function calculates the inverse of the Cumulative Log-Normal +// Distribution Function of x, for a supplied probability. The syntax of the +// function is: +// +// LOGNORM.INV(probability,mean,standard_dev) +// +func (fn *formulaFuncs) LOGNORMdotINV(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "LOGNORM.INV requires 3 arguments") + } + return fn.LOGINV(argsList) +} + // NORMdotDIST function calculates the Normal Probability Density Function or // the Cumulative Normal Distribution. Function for a supplied set of // parameters. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 64076856f5..8f1c1e56d5 100644 --- a/calc_test.go +++ b/calc_test.go @@ -890,6 +890,10 @@ func TestCalcCellValue(t *testing.T) { "=F.INV.RT(0.5,4,8)": "0.914645355977072", "=F.INV.RT(0.1,79,86)": "1.32646097270444", "=F.INV.RT(1,40,5)": "0", + // LOGINV + "=LOGINV(0.3,2,0.2)": "6.6533460753367", + // LOGINV + "=LOGNORM.INV(0.3,2,0.2)": "6.6533460753367", // NORM.DIST "=NORM.DIST(0.8,1,0.3,TRUE)": "0.252492537546923", "=NORM.DIST(50,40,20,FALSE)": "0.017603266338215", @@ -2487,6 +2491,22 @@ func TestCalcCellValue(t *testing.T) { "=F.INV.RT(0,1,2)": "#NUM!", "=F.INV.RT(0.2,0.5,2)": "#NUM!", "=F.INV.RT(0.2,1,0.5)": "#NUM!", + // LOGINV + "=LOGINV()": "LOGINV requires 3 arguments", + "=LOGINV(\"\",2,0.2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGINV(0.3,\"\",0.2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGINV(0.3,2,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGINV(0,2,0.2)": "#NUM!", + "=LOGINV(1,2,0.2)": "#NUM!", + "=LOGINV(0.3,2,0)": "#NUM!", + // LOGNORM.INV + "=LOGNORM.INV()": "LOGNORM.INV requires 3 arguments", + "=LOGNORM.INV(\"\",2,0.2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGNORM.INV(0.3,\"\",0.2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGNORM.INV(0.3,2,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGNORM.INV(0,2,0.2)": "#NUM!", + "=LOGNORM.INV(1,2,0.2)": "#NUM!", + "=LOGNORM.INV(0.3,2,0)": "#NUM!", // NORM.DIST "=NORM.DIST()": "NORM.DIST requires 4 arguments", // NORMDIST diff --git a/cell.go b/cell.go index b44ed820e5..c3b62dc212 100644 --- a/cell.go +++ b/cell.go @@ -673,8 +673,11 @@ type HyperlinkOpts struct { // SetCellHyperLink provides a function to set cell hyperlink by given // worksheet name and link URL address. LinkType defines two types of // hyperlink "External" for web site or "Location" for moving to one of cell -// in this workbook. Maximum limit hyperlinks in a worksheet is 65530. The -// below is example for external link. +// in this workbook. Maximum limit hyperlinks in a worksheet is 65530. This +// function is only used to set the hyperlink of the cell and doesn't affect +// the value of the cell. If you need to set the value of the cell, please use +// the other functions such as `SetCellStyle` or `SetSheetRow`. The below is +// example for external link. // // if err := f.SetCellHyperLink("Sheet1", "A3", // "https://github.com/xuri/excelize", "External"); err != nil { From c3424a9a0fc4f62b4884701e1430b420bbd8d0e4 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 16 Mar 2022 00:19:29 +0800 Subject: [PATCH 003/213] ref #65, new formula functions: LOGNORM.DIST and LOGNORMDIST --- calc.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 20 ++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/calc.go b/calc.go index 951b843776..9cadeb90fd 100644 --- a/calc.go +++ b/calc.go @@ -506,6 +506,8 @@ type formulaFuncs struct { // LOG // LOG10 // LOGINV +// LOGNORM.DIST +// LOGNORMDIST // LOGNORM.INV // LOOKUP // LOWER @@ -6473,6 +6475,72 @@ func (fn *formulaFuncs) LOGNORMdotINV(argsList *list.List) formulaArg { return fn.LOGINV(argsList) } +// LOGNORMdotDIST function calculates the Log-Normal Probability Density +// Function or the Cumulative Log-Normal Distribution Function for a supplied +// value of x. The syntax of the function is: +// +// LOGNORM.DIST(x,mean,standard_dev,cumulative) +// +func (fn *formulaFuncs) LOGNORMdotDIST(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "LOGNORM.DIST requires 4 arguments") + } + var x, mean, stdDev, cumulative formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if mean = argsList.Front().Next().Value.(formulaArg).ToNumber(); mean.Type != ArgNumber { + return mean + } + if stdDev = argsList.Back().Prev().Value.(formulaArg).ToNumber(); stdDev.Type != ArgNumber { + return stdDev + } + if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError { + return cumulative + } + if x.Number <= 0 || stdDev.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if cumulative.Number == 1 { + args := list.New() + args.PushBack(newNumberFormulaArg((math.Log(x.Number) - mean.Number) / stdDev.Number)) + args.PushBack(newNumberFormulaArg(0)) + args.PushBack(newNumberFormulaArg(1)) + args.PushBack(cumulative) + return fn.NORMDIST(args) + } + return newNumberFormulaArg((1 / (math.Sqrt(2*math.Pi) * stdDev.Number * x.Number)) * + math.Exp(0-(math.Pow((math.Log(x.Number)-mean.Number), 2)/(2*math.Pow(stdDev.Number, 2))))) + +} + +// LOGNORMDIST function calculates the Cumulative Log-Normal Distribution +// Function at a supplied value of x. The syntax of the function is: +// +// LOGNORMDIST(x,mean,standard_dev) +// +func (fn *formulaFuncs) LOGNORMDIST(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "LOGNORMDIST requires 3 arguments") + } + var x, mean, stdDev formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if mean = argsList.Front().Next().Value.(formulaArg).ToNumber(); mean.Type != ArgNumber { + return mean + } + if stdDev = argsList.Back().Value.(formulaArg).ToNumber(); stdDev.Type != ArgNumber { + return stdDev + } + if x.Number <= 0 || stdDev.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + args := list.New() + args.PushBack(newNumberFormulaArg((math.Log(x.Number) - mean.Number) / stdDev.Number)) + return fn.NORMSDIST(args) +} + // NORMdotDIST function calculates the Normal Probability Density Function or // the Cumulative Normal Distribution. Function for a supplied set of // parameters. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 8f1c1e56d5..458c3811d9 100644 --- a/calc_test.go +++ b/calc_test.go @@ -894,6 +894,11 @@ func TestCalcCellValue(t *testing.T) { "=LOGINV(0.3,2,0.2)": "6.6533460753367", // LOGINV "=LOGNORM.INV(0.3,2,0.2)": "6.6533460753367", + // LOGNORM.DIST + "=LOGNORM.DIST(0.5,10,5,FALSE)": "0.0162104821842127", + "=LOGNORM.DIST(12,10,5,TRUE)": "0.0664171147992077", + // LOGNORMDIST + "=LOGNORMDIST(12,10,5)": "0.0664171147992077", // NORM.DIST "=NORM.DIST(0.8,1,0.3,TRUE)": "0.252492537546923", "=NORM.DIST(50,40,20,FALSE)": "0.017603266338215", @@ -2507,6 +2512,21 @@ func TestCalcCellValue(t *testing.T) { "=LOGNORM.INV(0,2,0.2)": "#NUM!", "=LOGNORM.INV(1,2,0.2)": "#NUM!", "=LOGNORM.INV(0.3,2,0)": "#NUM!", + // LOGNORM.DIST + "=LOGNORM.DIST()": "LOGNORM.DIST requires 4 arguments", + "=LOGNORM.DIST(\"\",10,5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGNORM.DIST(0.5,\"\",5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGNORM.DIST(0.5,10,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGNORM.DIST(0.5,10,5,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=LOGNORM.DIST(0,10,5,FALSE)": "#NUM!", + "=LOGNORM.DIST(0.5,10,0,FALSE)": "#NUM!", + // LOGNORMDIST + "=LOGNORMDIST()": "LOGNORMDIST requires 3 arguments", + "=LOGNORMDIST(\"\",10,5)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGNORMDIST(12,\"\",5)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGNORMDIST(12,10,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LOGNORMDIST(0,2,5)": "#NUM!", + "=LOGNORMDIST(12,10,0)": "#NUM!", // NORM.DIST "=NORM.DIST()": "NORM.DIST requires 4 arguments", // NORMDIST From 1da129a3df144d69cfc67f05a4dec88063a774e8 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 17 Mar 2022 08:16:20 +0800 Subject: [PATCH 004/213] ref #65, new formula functions: CHIINV and BETADIST --- calc.go | 283 ++++++++++++++++++++++++++++++++++++++++++++++++++- calc_test.go | 48 +++++++++ 2 files changed, 329 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index 9cadeb90fd..9dc430d3d2 100644 --- a/calc.go +++ b/calc.go @@ -333,6 +333,7 @@ type formulaFuncs struct { // BESSELJ // BESSELK // BESSELY +// BETADIST // BETAINV // BETA.INV // BIN2DEC @@ -348,6 +349,7 @@ type formulaFuncs struct { // CEILING.PRECISE // CHAR // CHIDIST +// CHIINV // CHOOSE // CLEAN // CODE @@ -5200,6 +5202,257 @@ func (fn *formulaFuncs) AVERAGEIF(argsList *list.List) formulaArg { return newNumberFormulaArg(sum / count) } +// getBetaHelperContFrac continued fractions for the beta function. +func getBetaHelperContFrac(fX, fA, fB float64) float64 { + var a1, b1, a2, b2, fnorm, cfnew, cf, rm float64 + a1, b1, b2 = 1, 1, 1-(fA+fB)/(fA+1)*fX + if b2 == 0 { + a2, fnorm, cf = 0, 1, 1 + } else { + a2, fnorm = 1, 1/b2 + cf = a2 * fnorm + } + cfnew, rm = 1, 1 + fMaxIter, fMachEps := 50000.0, 2.22045e-016 + bfinished := false + for rm < fMaxIter && !bfinished { + apl2m := fA + 2*rm + d2m := rm * (fB - rm) * fX / ((apl2m - 1) * apl2m) + d2m1 := -(fA + rm) * (fA + fB + rm) * fX / (apl2m * (apl2m + 1)) + a1 = (a2 + d2m*a1) * fnorm + b1 = (b2 + d2m*b1) * fnorm + a2 = a1 + d2m1*a2*fnorm + b2 = b1 + d2m1*b2*fnorm + if b2 != 0 { + fnorm = 1 / b2 + cfnew = a2 * fnorm + bfinished = (math.Abs(cf-cfnew) < math.Abs(cf)*fMachEps) + } + cf = cfnew + rm += 1 + } + return cf +} + +// getLanczosSum uses a variant of the Lanczos sum with a rational function. +func getLanczosSum(fZ float64) float64 { + num := []float64{ + 23531376880.41075968857200767445163675473, + 42919803642.64909876895789904700198885093, + 35711959237.35566804944018545154716670596, + 17921034426.03720969991975575445893111267, + 6039542586.35202800506429164430729792107, + 1439720407.311721673663223072794912393972, + 248874557.8620541565114603864132294232163, + 31426415.58540019438061423162831820536287, + 2876370.628935372441225409051620849613599, + 186056.2653952234950402949897160456992822, + 8071.672002365816210638002902272250613822, + 210.8242777515793458725097339207133627117, + 2.506628274631000270164908177133837338626, + } + denom := []float64{ + 0, + 39916800, + 120543840, + 150917976, + 105258076, + 45995730, + 13339535, + 2637558, + 357423, + 32670, + 1925, + 66, + 1, + } + var sumNum, sumDenom, zInv float64 + if fZ <= 1 { + sumNum = num[12] + sumDenom = denom[12] + for i := 11; i >= 0; i-- { + sumNum *= fZ + sumNum += num[i] + sumDenom *= fZ + sumDenom += denom[i] + } + } else { + zInv = 1 / fZ + sumNum = num[0] + sumDenom = denom[0] + for i := 1; i <= 12; i++ { + sumNum *= zInv + sumNum += num[i] + sumDenom *= zInv + sumDenom += denom[i] + } + } + return sumNum / sumDenom +} + +// getBeta return beta distribution. +func getBeta(fAlpha, fBeta float64) float64 { + var fA, fB float64 + if fAlpha > fBeta { + fA = fAlpha + fB = fBeta + } else { + fA = fBeta + fB = fAlpha + } + const maxGammaArgument = 171.624376956302 + if fA+fB < maxGammaArgument { + return math.Gamma(fA) / math.Gamma(fA+fB) * math.Gamma(fB) + } + fg := 6.024680040776729583740234375 + fgm := fg - 0.5 + fLanczos := getLanczosSum(fA) + fLanczos /= getLanczosSum(fA + fB) + fLanczos *= getLanczosSum(fB) + fABgm := fA + fB + fgm + fLanczos *= math.Sqrt((fABgm / (fA + fgm)) / (fB + fgm)) + fTempA := fB / (fA + fgm) + fTempB := fA / (fB + fgm) + fResult := math.Exp(-fA*math.Log1p(fTempA) - fB*math.Log1p(fTempB) - fgm) + fResult *= fLanczos + return fResult +} + +// getBetaDistPDF is an implementation for the Beta probability density +// function. +func getBetaDistPDF(fX, fA, fB float64) float64 { + if fX <= 0 || fX >= 1 { + return 0 + } + fLogDblMax, fLogDblMin := math.Log(1.79769e+308), math.Log(2.22507e-308) + fLogY := math.Log(0.5 - fX + 0.5) + if fX < 0.1 { + fLogY = math.Log1p(-fX) + } + fLogX := math.Log(fX) + fAm1LogX := (fA - 1) * fLogX + fBm1LogY := (fB - 1) * fLogY + fLogBeta := getLogBeta(fA, fB) + if fAm1LogX < fLogDblMax && fAm1LogX > fLogDblMin && fBm1LogY < fLogDblMax && + fBm1LogY > fLogDblMin && fLogBeta < fLogDblMax && fLogBeta > fLogDblMin && + fAm1LogX+fBm1LogY < fLogDblMax && fAm1LogX+fBm1LogY > fLogDblMin { + return math.Pow(fX, fA-1) * math.Pow(0.5-fX+0.5, fB-1) / getBeta(fA, fB) + } + return math.Exp(fAm1LogX + fBm1LogY - fLogBeta) +} + +// getLogBeta return beta with logarithm. +func getLogBeta(fAlpha, fBeta float64) float64 { + var fA, fB float64 + if fAlpha > fBeta { + fA, fB = fAlpha, fBeta + } else { + fA, fB = fBeta, fAlpha + } + fg := 6.024680040776729583740234375 + fgm := fg - 0.5 + fLanczos := getLanczosSum(fA) + fLanczos /= getLanczosSum(fA + fB) + fLanczos *= getLanczosSum(fB) + fLogLanczos := math.Log(fLanczos) + fABgm := fA + fB + fgm + fLogLanczos += 0.5 * (math.Log(fABgm) - math.Log(fA+fgm) - math.Log(fB+fgm)) + fTempA := fB / (fA + fgm) + fTempB := fA / (fB + fgm) + fResult := -fA*math.Log1p(fTempA) - fB*math.Log1p(fTempB) - fgm + fResult += fLogLanczos + return fResult +} + +// getBetaDist is an implementation for the beta distribution function. +func getBetaDist(fXin, fAlpha, fBeta float64) float64 { + if fXin <= 0 { + return 0 + } + if fXin >= 1 { + return 1 + } + if fBeta == 1 { + return math.Pow(fXin, fAlpha) + } + if fAlpha == 1 { + return -math.Expm1(fBeta * math.Log1p(-fXin)) + } + var fResult float64 + fY, flnY := (0.5-fXin)+0.5, math.Log1p(-fXin) + fX, flnX := fXin, math.Log(fXin) + fA, fB := fAlpha, fBeta + bReflect := fXin > fAlpha/(fAlpha+fBeta) + if bReflect { + fA = fBeta + fB = fAlpha + fX = fY + fY = fXin + flnX = flnY + flnY = math.Log(fXin) + } + fResult = getBetaHelperContFrac(fX, fA, fB) / fA + fP, fQ := fA/(fA+fB), fB/(fA+fB) + var fTemp float64 + if fA > 1 && fB > 1 && fP < 0.97 && fQ < 0.97 { + fTemp = getBetaDistPDF(fX, fA, fB) * fX * fY + } else { + fTemp = math.Exp(fA*flnX + fB*flnY - getLogBeta(fA, fB)) + } + fResult *= fTemp + if bReflect { + fResult = 0.5 - fResult + 0.5 + } + return fResult +} + +// BETADIST function calculates the cumulative beta probability density +// function for a supplied set of parameters. The syntax of the function is: +// +// BETADIST(x,alpha,beta,[A],[B]) +// +func (fn *formulaFuncs) BETADIST(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "BETADIST requires at least 3 arguments") + } + if argsList.Len() > 5 { + return newErrorFormulaArg(formulaErrorVALUE, "BETADIST requires at most 5 arguments") + } + x := argsList.Front().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + alpha := argsList.Front().Next().Value.(formulaArg).ToNumber() + if alpha.Type != ArgNumber { + return alpha + } + beta := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if beta.Type != ArgNumber { + return beta + } + if alpha.Number <= 0 || beta.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + a, b := newNumberFormulaArg(0), newNumberFormulaArg(1) + if argsList.Len() > 3 { + if a = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); a.Type != ArgNumber { + return a + } + } + if argsList.Len() == 5 { + if b = argsList.Back().Value.(formulaArg).ToNumber(); b.Type != ArgNumber { + return b + } + } + if x.Number < a.Number || x.Number > b.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if a.Number == b.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(getBetaDist((x.Number-a.Number)/(b.Number-a.Number), alpha.Number, beta.Number)) +} + // d1mach returns double precision real machine constants. func d1mach(i int) float64 { arr := []float64{ @@ -5603,6 +5856,32 @@ func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { return newNumberFormulaArg(1 - (incompleteGamma(degress.Number/2, x.Number/2) / math.Gamma(degress.Number/2))) } +// CHIINV function calculates the inverse of the right-tailed probability of +// the Chi-Square Distribution. The syntax of the function is: +// +// CHIINV(probability,degrees_freedom) +// +func (fn *formulaFuncs) CHIINV(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "CHIINV requires 2 numeric arguments") + } + probability := argsList.Front().Value.(formulaArg).ToNumber() + if probability.Type != ArgNumber { + return probability + } + if probability.Number <= 0 || probability.Number > 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + degress := argsList.Back().Value.(formulaArg).ToNumber() + if degress.Type != ArgNumber { + return degress + } + if degress.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(gammainv(1-probability.Number, 0.5*degress.Number, 2.0)) +} + // confidence is an implementation of the formula functions CONFIDENCE and // CONFIDENCE.NORM. func (fn *formulaFuncs) confidence(name string, argsList *list.List) formulaArg { @@ -6511,7 +6790,6 @@ func (fn *formulaFuncs) LOGNORMdotDIST(argsList *list.List) formulaArg { } return newNumberFormulaArg((1 / (math.Sqrt(2*math.Pi) * stdDev.Number * x.Number)) * math.Exp(0-(math.Pow((math.Log(x.Number)-mean.Number), 2)/(2*math.Pow(stdDev.Number, 2))))) - } // LOGNORMDIST function calculates the Cumulative Log-Normal Distribution @@ -10812,7 +11090,8 @@ func (fn *formulaFuncs) prepareXlookupArgs(argsList *list.List) formulaArg { // xlookup is an implementation of the formula function XLOOKUP. func (fn *formulaFuncs) xlookup(lookupRows, lookupCols, returnArrayRows, returnArrayCols, matchIdx int, - condition1, condition2, condition3, condition4 bool, returnArray formulaArg) formulaArg { + condition1, condition2, condition3, condition4 bool, returnArray formulaArg, +) formulaArg { result := [][]formulaArg{} for rowIdx, row := range returnArray.Matrix { for colIdx, cell := range row { diff --git a/calc_test.go b/calc_test.go index 458c3811d9..6e40d2aae9 100644 --- a/calc_test.go +++ b/calc_test.go @@ -784,6 +784,20 @@ func TestCalcCellValue(t *testing.T) { "=AVERAGEA(A1)": "1", "=AVERAGEA(A1:A2)": "1.5", "=AVERAGEA(D2:F9)": "12671.375", + // BETADIST + "=BETADIST(0.4,4,5)": "0.4059136", + "=BETADIST(0.4,4,5,0,1)": "0.4059136", + "=BETADIST(0.4,4,5,0.4,1)": "0", + "=BETADIST(1,2,2,1,3)": "0", + "=BETADIST(0.4,4,5,0.2,0.4)": "1", + "=BETADIST(0.4,4,1)": "0.0256", + "=BETADIST(0.4,1,5)": "0.92224", + "=BETADIST(3,4,6,2,4)": "0.74609375", + "=BETADIST(0.4,2,100)": "1", + "=BETADIST(0.75,3,4)": "0.96240234375", + "=BETADIST(0.2,0.7,4)": "0.71794309318323", + "=BETADIST(0.01,3,4)": "1.955359E-05", + "=BETADIST(0.75,130,140)": "1", // BETAINV "=BETAINV(0.2,4,5,0,1)": "0.303225844664082", // BETA.INV @@ -791,6 +805,11 @@ func TestCalcCellValue(t *testing.T) { // CHIDIST "=CHIDIST(0.5,3)": "0.918891411654676", "=CHIDIST(8,3)": "0.0460117056892315", + // CHIINV + "=CHIINV(0.5,1)": "0.454936423119572", + "=CHIINV(0.75,1)": "0.101531044267622", + "=CHIINV(0.1,2)": "4.605170185988088", + "=CHIINV(0.8,2)": "0.446287102628419", // CONFIDENCE "=CONFIDENCE(0.05,0.07,100)": "0.0137197479028414", // CONFIDENCE.NORM @@ -2322,6 +2341,19 @@ func TestCalcCellValue(t *testing.T) { "=AVERAGE(H1)": "AVERAGE divide by zero", // AVERAGEA "=AVERAGEA(H1)": "AVERAGEA divide by zero", + // BETADIST + "=BETADIST()": "BETADIST requires at least 3 arguments", + "=BETADIST(0.4,4,5,0,1,0)": "BETADIST requires at most 5 arguments", + "=BETADIST(\"\",4,5,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BETADIST(0.4,\"\",5,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BETADIST(0.4,4,\"\",0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BETADIST(0.4,4,5,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BETADIST(0.4,4,5,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BETADIST(2,4,5,3,1)": "#NUM!", + "=BETADIST(2,4,5,0,1)": "#NUM!", + "=BETADIST(0.4,0,5,0,1)": "#NUM!", + "=BETADIST(0.4,4,0,0,1)": "#NUM!", + "=BETADIST(0.4,4,5,0.4,0.4)": "#NUM!", // BETAINV "=BETAINV()": "BETAINV requires at least 3 arguments", "=BETAINV(0.2,4,5,0,1,0)": "BETAINV requires at most 5 arguments", @@ -2357,6 +2389,13 @@ func TestCalcCellValue(t *testing.T) { "=CHIDIST()": "CHIDIST requires 2 numeric arguments", "=CHIDIST(\"\",3)": "strconv.ParseFloat: parsing \"\": invalid syntax", "=CHIDIST(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + // CHIINV + "=CHIINV()": "CHIINV requires 2 numeric arguments", + "=CHIINV(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHIINV(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHIINV(0,1)": "#NUM!", + "=CHIINV(2,1)": "#NUM!", + "=CHIINV(0.5,0.5)": "#NUM!", // CONFIDENCE "=CONFIDENCE()": "CONFIDENCE requires 3 numeric arguments", "=CONFIDENCE(\"\",0.07,100)": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -4388,6 +4427,15 @@ func TestGetYearDays(t *testing.T) { } } +func TestCalcGetBetaHelperContFrac(t *testing.T) { + assert.Equal(t, 1.0, getBetaHelperContFrac(1, 0, 1)) +} + +func TestCalcGetBetaDistPDF(t *testing.T) { + assert.Equal(t, 0.0, getBetaDistPDF(0.5, 2000, 3)) + assert.Equal(t, 0.0, getBetaDistPDF(0, 1, 0)) +} + func TestCalcD1mach(t *testing.T) { assert.Equal(t, 0.0, d1mach(6)) } From 14b461420fc3d3b06b01d7b0584b422b3e1b40fb Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 18 Mar 2022 00:52:10 +0800 Subject: [PATCH 005/213] This fix scientific notation and page setup fields parsing issue --- calc_test.go | 23 +++++++++++------------ lib.go | 3 ++- rows.go | 2 +- xmlWorksheet.go | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/calc_test.go b/calc_test.go index 6e40d2aae9..6f08554874 100644 --- a/calc_test.go +++ b/calc_test.go @@ -66,7 +66,7 @@ func TestCalcCellValue(t *testing.T) { // Engineering Functions // BESSELI "=BESSELI(4.5,1)": "15.389222753735925", - "=BESSELI(32,1)": "5.502845511211247e+12", + "=BESSELI(32,1)": "5502845511211.25", // BESSELJ "=BESSELJ(1.9,2)": "0.329925727692387", // BESSELK @@ -457,7 +457,7 @@ func TestCalcCellValue(t *testing.T) { "=EVEN(-4)": "-4", "=EVEN((0))": "0", // EXP - "=EXP(100)": "2.6881171418161356E+43", + "=EXP(100)": "2.68811714181614E+43", "=EXP(0.1)": "1.10517091807565", "=EXP(0)": "1", "=EXP(-5)": "0.00673794699908547", @@ -465,7 +465,7 @@ func TestCalcCellValue(t *testing.T) { // FACT "=FACT(3)": "6", "=FACT(6)": "720", - "=FACT(10)": "3.6288e+06", + "=FACT(10)": "3628800", "=FACT(FACT(3))": "720", // FACTDOUBLE "=FACTDOUBLE(5)": "15", @@ -915,9 +915,9 @@ func TestCalcCellValue(t *testing.T) { "=LOGNORM.INV(0.3,2,0.2)": "6.6533460753367", // LOGNORM.DIST "=LOGNORM.DIST(0.5,10,5,FALSE)": "0.0162104821842127", - "=LOGNORM.DIST(12,10,5,TRUE)": "0.0664171147992077", + "=LOGNORM.DIST(12,10,5,TRUE)": "0.0664171147992078", // LOGNORMDIST - "=LOGNORMDIST(12,10,5)": "0.0664171147992077", + "=LOGNORMDIST(12,10,5)": "0.0664171147992078", // NORM.DIST "=NORM.DIST(0.8,1,0.3,TRUE)": "0.252492537546923", "=NORM.DIST(50,40,20,FALSE)": "0.017603266338215", @@ -1304,12 +1304,12 @@ func TestCalcCellValue(t *testing.T) { "=TIME(\"5\",\"44\",\"32\")": "0.239259259259259", "=TIME(0,0,73)": "0.000844907407407407", // TIMEVALUE - "=TIMEVALUE(\"2:23\")": "0.0993055555555556", - "=TIMEVALUE(\"2:23 am\")": "0.0993055555555556", - "=TIMEVALUE(\"2:23 PM\")": "0.599305555555555", - "=TIMEVALUE(\"14:23:00\")": "0.599305555555555", + "=TIMEVALUE(\"2:23\")": "0.0993055555555555", + "=TIMEVALUE(\"2:23 am\")": "0.0993055555555555", + "=TIMEVALUE(\"2:23 PM\")": "0.599305555555556", + "=TIMEVALUE(\"14:23:00\")": "0.599305555555556", "=TIMEVALUE(\"00:02:23\")": "0.00165509259259259", - "=TIMEVALUE(\"01/01/2011 02:23\")": "0.0993055555555556", + "=TIMEVALUE(\"01/01/2011 02:23\")": "0.0993055555555555", // WEEKDAY "=WEEKDAY(0)": "7", "=WEEKDAY(47119)": "2", @@ -4137,8 +4137,7 @@ func TestCalcXIRR(t *testing.T) { f := prepareCalcData(cellData) formulaList := map[string]string{ "=XIRR(A1:A4,B1:B4)": "-0.196743861298328", - "=XIRR(A1:A6,B1:B6)": "0.09443907444452", - "=XIRR(A1:A6,B1:B6,0.1)": "0.0944390744445201", + "=XIRR(A1:A6,B1:B6,0.5)": "0.0944390744445204", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) diff --git a/lib.go b/lib.go index d0ae62c3b0..5bbbec9cae 100644 --- a/lib.go +++ b/lib.go @@ -688,7 +688,8 @@ func isNumeric(s string) (bool, int) { if i == 0 && v == '-' { continue } - if e && v == '-' { + if e && (v == '+' || v == '-') { + p = 15 continue } return false, 0 diff --git a/rows.go b/rows.go index 81eaeeb823..ec94c644ec 100644 --- a/rows.go +++ b/rows.go @@ -471,7 +471,7 @@ func roundPrecision(text string, prec int) string { if _, ok := decimal.SetString(text); ok { flt, _ := decimal.Float64() if prec == -1 { - return decimal.Text('G', 15) + return strconv.FormatFloat(flt, 'G', 15, 64) } return strconv.FormatFloat(flt, 'f', -1, 64) } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index c327d3c4ac..13deba56fd 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -116,7 +116,7 @@ type xlsxPageSetUp struct { FirstPageNumber string `xml:"firstPageNumber,attr,omitempty"` FitToHeight *int `xml:"fitToHeight,attr"` FitToWidth *int `xml:"fitToWidth,attr"` - HorizontalDPI int `xml:"horizontalDpi,attr,omitempty"` + HorizontalDPI float64 `xml:"horizontalDpi,attr,omitempty"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` Orientation string `xml:"orientation,attr,omitempty"` PageOrder string `xml:"pageOrder,attr,omitempty"` @@ -126,7 +126,7 @@ type xlsxPageSetUp struct { Scale int `xml:"scale,attr,omitempty"` UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` - VerticalDPI int `xml:"verticalDpi,attr,omitempty"` + VerticalDPI float64 `xml:"verticalDpi,attr,omitempty"` } // xlsxPrintOptions directly maps the printOptions element in the namespace From 94f197c4fe6531f96a42fe4e960c1c921a3ee0e8 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 19 Mar 2022 00:05:47 +0800 Subject: [PATCH 006/213] This improved formula calculate precision and added zero placeholder number format support --- .github/workflows/go.yml | 2 +- calc_test.go | 200 +++++++++++++++++++-------------------- cell.go | 22 ++--- file.go | 2 +- lib.go | 7 +- lib_test.go | 4 +- numfmt.go | 52 ++++++++-- numfmt_test.go | 8 ++ picture_test.go | 4 +- rows.go | 7 ++ 10 files changed, 178 insertions(+), 130 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b9841144cb..8310222581 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,7 +5,7 @@ jobs: test: strategy: matrix: - go-version: [1.15.x, 1.16.x, 1.17.x] + go-version: [1.15.x, 1.16.x, 1.17.x, 1.18.x] os: [ubuntu-latest, macos-latest, windows-latest] targetplatform: [x86, x64] diff --git a/calc_test.go b/calc_test.go index 6f08554874..d350037926 100644 --- a/calc_test.go +++ b/calc_test.go @@ -65,19 +65,19 @@ func TestCalcCellValue(t *testing.T) { `="A"<>"A"`: "FALSE", // Engineering Functions // BESSELI - "=BESSELI(4.5,1)": "15.389222753735925", + "=BESSELI(4.5,1)": "15.3892227537359", "=BESSELI(32,1)": "5502845511211.25", // BESSELJ "=BESSELJ(1.9,2)": "0.329925727692387", // BESSELK "=BESSELK(0.05,0)": "3.11423403428966", - "=BESSELK(0.05,1)": "19.90967432724863", + "=BESSELK(0.05,1)": "19.9096743272486", "=BESSELK(0.05,2)": "799.501207124235", "=BESSELK(3,2)": "0.0615104585619118", // BESSELY "=BESSELY(0.05,0)": "-1.97931100684153", - "=BESSELY(0.05,1)": "-12.789855163794034", - "=BESSELY(0.05,2)": "-509.61489554491976", + "=BESSELY(0.05,1)": "-12.789855163794", + "=BESSELY(0.05,2)": "-509.61489554492", "=BESSELY(9,2)": "-0.229082087487741", // BIN2DEC "=BIN2DEC(\"10\")": "2", @@ -213,7 +213,7 @@ func TestCalcCellValue(t *testing.T) { "=IMCOSH(\"2-i\")": "2.0327230070196656-3.0518977991518i", "=IMCOSH(COMPLEX(1,-1))": "0.8337300251311491-0.9888977057628651i", // IMCOT - "=IMCOT(0.5)": "1.830487721712452", + "=IMCOT(0.5)": "1.83048772171245", "=IMCOT(\"3+0.5i\")": "-0.4793455787473728-2.016092521506228i", "=IMCOT(\"2-i\")": "-0.171383612909185+0.8213297974938518i", "=IMCOT(COMPLEX(1,-1))": "0.21762156185440268+0.868014142895925i", @@ -247,7 +247,7 @@ func TestCalcCellValue(t *testing.T) { "=IMREAL(\"3i\")": "0", "=IMREAL(COMPLEX(4,1))": "4", // IMSEC - "=IMSEC(0.5)": "1.139493927324549", + "=IMSEC(0.5)": "1.13949392732455", "=IMSEC(\"3+0.5i\")": "-0.8919131797403304+0.05875317818173977i", "=IMSEC(\"2-i\")": "-0.4131493442669401-0.687527438655479i", "=IMSEC(COMPLEX(1,-1))": "0.49833703055518686-0.5910838417210451i", @@ -271,7 +271,7 @@ func TestCalcCellValue(t *testing.T) { "=IMSQRT(\"i\")": "0.7071067811865476+0.7071067811865476i", "=IMSQRT(\"2-i\")": "1.455346690225355-0.34356074972251244i", "=IMSQRT(\"5+2i\")": "2.27872385417085+0.4388421169022545i", - "=IMSQRT(6)": "2.449489742783178", + "=IMSQRT(6)": "2.44948974278318", "=IMSQRT(\"-2-4i\")": "1.1117859405028423-1.7989074399478673i", // IMSUB "=IMSUB(\"5+i\",\"1+4i\")": "4-3i", @@ -313,17 +313,17 @@ func TestCalcCellValue(t *testing.T) { "=ABS(2-4.5)": "2.5", "=ABS(ABS(-1))": "1", // ACOS - "=ACOS(-1)": "3.141592653589793", + "=ACOS(-1)": "3.14159265358979", "=ACOS(0)": "1.5707963267949", "=ACOS(ABS(0))": "1.5707963267949", // ACOSH "=ACOSH(1)": "0", - "=ACOSH(2.5)": "1.566799236972411", + "=ACOSH(2.5)": "1.56679923697241", "=ACOSH(5)": "2.29243166956118", "=ACOSH(ACOSH(5))": "1.47138332153668", // ACOT "=_xlfn.ACOT(1)": "0.785398163397448", - "=_xlfn.ACOT(-2)": "2.677945044588987", + "=_xlfn.ACOT(-2)": "2.67794504458899", "=_xlfn.ACOT(0)": "1.5707963267949", "=_xlfn.ACOT(_xlfn.ACOT(0))": "0.566911504941009", // ACOTH @@ -445,9 +445,9 @@ func TestCalcCellValue(t *testing.T) { `=_xlfn.DECIMAL("70122",8)`: "28754", `=_xlfn.DECIMAL("0x70122",8)`: "28754", // DEGREES - "=DEGREES(1)": "57.29577951308232", - "=DEGREES(2.5)": "143.2394487827058", - "=DEGREES(DEGREES(1))": "3282.806350011744", + "=DEGREES(1)": "57.2957795130823", + "=DEGREES(2.5)": "143.239448782706", + "=DEGREES(DEGREES(1))": "3282.80635001174", // EVEN "=EVEN(23)": "24", "=EVEN(2.22)": "4", @@ -461,7 +461,7 @@ func TestCalcCellValue(t *testing.T) { "=EXP(0.1)": "1.10517091807565", "=EXP(0)": "1", "=EXP(-5)": "0.00673794699908547", - "=EXP(EXP(0))": "2.718281828459045", + "=EXP(EXP(0))": "2.71828182845905", // FACT "=FACT(3)": "6", "=FACT(6)": "720", @@ -473,12 +473,12 @@ func TestCalcCellValue(t *testing.T) { "=FACTDOUBLE(13)": "135135", "=FACTDOUBLE(FACTDOUBLE(1))": "1", // FLOOR - "=FLOOR(26.75,0.1)": "26.700000000000003", + "=FLOOR(26.75,0.1)": "26.7", "=FLOOR(26.75,0.5)": "26.5", "=FLOOR(26.75,1)": "26", "=FLOOR(26.75,10)": "20", "=FLOOR(26.75,20)": "20", - "=FLOOR(-26.75,-0.1)": "-26.700000000000003", + "=FLOOR(-26.75,-0.1)": "-26.7", "=FLOOR(-26.75,-1)": "-26", "=FLOOR(-26.75,-5)": "-25", "=FLOOR(FLOOR(26.75,1),1)": "26", @@ -493,7 +493,7 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.FLOOR.MATH(-58.55,10)": "-60", "=_xlfn.FLOOR.MATH(_xlfn.FLOOR.MATH(1),10)": "0", // _xlfn.FLOOR.PRECISE - "=_xlfn.FLOOR.PRECISE(26.75,0.1)": "26.700000000000003", + "=_xlfn.FLOOR.PRECISE(26.75,0.1)": "26.7", "=_xlfn.FLOOR.PRECISE(26.75,0.5)": "26.5", "=_xlfn.FLOOR.PRECISE(26.75,1)": "26", "=_xlfn.FLOOR.PRECISE(26.75)": "26", @@ -524,7 +524,7 @@ func TestCalcCellValue(t *testing.T) { "=ISO.CEILING(22.25,0.1)": "22.3", "=ISO.CEILING(22.25,10)": "30", "=ISO.CEILING(-22.25,1)": "-22", - "=ISO.CEILING(-22.25,0.1)": "-22.200000000000003", + "=ISO.CEILING(-22.25,0.1)": "-22.2", "=ISO.CEILING(-22.25,5)": "-20", "=ISO.CEILING(-22.25,0)": "0", "=ISO.CEILING(1,ISO.CEILING(1,0))": "0", @@ -539,7 +539,7 @@ func TestCalcCellValue(t *testing.T) { `=LCM(0,LCM(0,0))`: "0", // LN "=LN(1)": "0", - "=LN(100)": "4.605170185988092", + "=LN(100)": "4.60517018598809", "=LN(0.5)": "-0.693147180559945", "=LN(LN(100))": "1.5271796258079", // LOG @@ -557,7 +557,7 @@ func TestCalcCellValue(t *testing.T) { // IMLOG2 "=IMLOG2(\"5+2i\")": "2.4289904975637864+0.5489546632866347i", "=IMLOG2(\"2-i\")": "1.1609640474436813-0.6689021062254881i", - "=IMLOG2(6)": "2.584962500721156", + "=IMLOG2(6)": "2.58496250072116", "=IMLOG2(\"3i\")": "1.584962500721156+2.266180070913597i", "=IMLOG2(\"4+i\")": "2.04373142062517+0.3534295024167349i", // IMPOWER @@ -604,7 +604,7 @@ func TestCalcCellValue(t *testing.T) { "=ODD(-3)": "-3", "=ODD(ODD(1))": "1", // PI - "=PI()": "3.141592653589793", + "=PI()": "3.14159265358979", // POWER "=POWER(4,2)": "16", "=POWER(4,POWER(1,1))": "4", @@ -619,9 +619,9 @@ func TestCalcCellValue(t *testing.T) { "=QUOTIENT(QUOTIENT(1,2),3)": "0", // RADIANS "=RADIANS(50)": "0.872664625997165", - "=RADIANS(-180)": "-3.141592653589793", - "=RADIANS(180)": "3.141592653589793", - "=RADIANS(360)": "6.283185307179586", + "=RADIANS(-180)": "-3.14159265358979", + "=RADIANS(180)": "3.14159265358979", + "=RADIANS(360)": "6.28318530717959", "=RADIANS(RADIANS(360))": "0.109662271123215", // ROMAN "=ROMAN(499,0)": "CDXCIX", @@ -634,9 +634,9 @@ func TestCalcCellValue(t *testing.T) { "=ROMAN(1999,5)": "MIM", "=ROMAN(1999,ODD(1))": "MLMVLIV", // ROUND - "=ROUND(100.319,1)": "100.30000000000001", - "=ROUND(5.28,1)": "5.300000000000001", - "=ROUND(5.9999,3)": "6.000000000000002", + "=ROUND(100.319,1)": "100.3", + "=ROUND(5.28,1)": "5.3", + "=ROUND(5.9999,3)": "6", "=ROUND(99.5,0)": "100", "=ROUND(-6.3,0)": "-6", "=ROUND(-100.5,0)": "-101", @@ -646,18 +646,18 @@ func TestCalcCellValue(t *testing.T) { "=ROUND(ROUND(100,1),-1)": "100", // ROUNDDOWN "=ROUNDDOWN(99.999,1)": "99.9", - "=ROUNDDOWN(99.999,2)": "99.99000000000002", + "=ROUNDDOWN(99.999,2)": "99.99", "=ROUNDDOWN(99.999,0)": "99", "=ROUNDDOWN(99.999,-1)": "90", - "=ROUNDDOWN(-99.999,2)": "-99.99000000000002", + "=ROUNDDOWN(-99.999,2)": "-99.99", "=ROUNDDOWN(-99.999,-1)": "-90", "=ROUNDDOWN(ROUNDDOWN(100,1),-1)": "100", // ROUNDUP` - "=ROUNDUP(11.111,1)": "11.200000000000001", - "=ROUNDUP(11.111,2)": "11.120000000000003", + "=ROUNDUP(11.111,1)": "11.2", + "=ROUNDUP(11.111,2)": "11.12", "=ROUNDUP(11.111,0)": "12", "=ROUNDUP(11.111,-1)": "20", - "=ROUNDUP(-11.111,2)": "-11.120000000000003", + "=ROUNDUP(-11.111,2)": "-11.12", "=ROUNDUP(-11.111,-1)": "-20", "=ROUNDUP(ROUNDUP(100,1),-1)": "100", // SEC @@ -681,26 +681,26 @@ func TestCalcCellValue(t *testing.T) { // SINH "=SINH(0)": "0", "=SINH(0.5)": "0.521095305493747", - "=SINH(-2)": "-3.626860407847019", + "=SINH(-2)": "-3.62686040784702", "=SINH(SINH(0))": "0", // SQRT "=SQRT(4)": "2", "=SQRT(SQRT(16))": "2", // SQRTPI - "=SQRTPI(5)": "3.963327297606011", + "=SQRTPI(5)": "3.96332729760601", "=SQRTPI(0.2)": "0.792665459521202", - "=SQRTPI(100)": "17.72453850905516", + "=SQRTPI(100)": "17.7245385090552", "=SQRTPI(0)": "0", "=SQRTPI(SQRTPI(0))": "0", // STDEV - "=STDEV(F2:F9)": "10724.978287523809", + "=STDEV(F2:F9)": "10724.9782875238", "=STDEV(MUNIT(2))": "0.577350269189626", "=STDEV(0,INT(0))": "0", "=STDEV(INT(1),INT(1))": "0", // STDEV.S - "=STDEV.S(F2:F9)": "10724.978287523809", + "=STDEV.S(F2:F9)": "10724.9782875238", // STDEVA - "=STDEVA(F2:F9)": "10724.978287523809", + "=STDEVA(F2:F9)": "10724.9782875238", "=STDEVA(MUNIT(2))": "0.577350269189626", "=STDEVA(0,INT(0))": "0", // POISSON.DIST @@ -723,7 +723,7 @@ func TestCalcCellValue(t *testing.T) { "=SUM(SUM(1+2/1)*2-3/2,2)": "6.5", "=((3+5*2)+3)/5+(-6)/4*2+3": "3.2", "=1+SUM(SUM(1,2*3),4)*-4/2+5+(4+2)*3": "2", - "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.666666666666664", + "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.6666666666667", "=SUM(1+ROW())": "2", "=SUM((SUM(2))+1)": "3", // SUMIF @@ -754,7 +754,7 @@ func TestCalcCellValue(t *testing.T) { // SUMXMY2 "=SUMXMY2(A1:A4,B1:B4)": "18", // TAN - "=TAN(1.047197551)": "1.732050806782486", + "=TAN(1.047197551)": "1.73205080678249", "=TAN(0)": "0", "=TAN(TAN(0))": "0", // TANH @@ -796,7 +796,7 @@ func TestCalcCellValue(t *testing.T) { "=BETADIST(0.4,2,100)": "1", "=BETADIST(0.75,3,4)": "0.96240234375", "=BETADIST(0.2,0.7,4)": "0.71794309318323", - "=BETADIST(0.01,3,4)": "1.955359E-05", + "=BETADIST(0.01,3,4)": "1.9553589999999998e-05", "=BETADIST(0.75,130,140)": "1", // BETAINV "=BETAINV(0.2,4,5,0,1)": "0.303225844664082", @@ -808,7 +808,7 @@ func TestCalcCellValue(t *testing.T) { // CHIINV "=CHIINV(0.5,1)": "0.454936423119572", "=CHIINV(0.75,1)": "0.101531044267622", - "=CHIINV(0.1,2)": "4.605170185988088", + "=CHIINV(0.1,2)": "4.60517018598809", "=CHIINV(0.8,2)": "0.446287102628419", // CONFIDENCE "=CONFIDENCE(0.05,0.07,100)": "0.0137197479028414", @@ -850,10 +850,10 @@ func TestCalcCellValue(t *testing.T) { "=FISHERINV(INT(0))": "0", "=FISHERINV(2.8)": "0.992631520201128", // GAMMA - "=GAMMA(0.1)": "9.513507698668732", + "=GAMMA(0.1)": "9.51350769866873", "=GAMMA(INT(1))": "1", "=GAMMA(1.5)": "0.886226925452758", - "=GAMMA(5.5)": "52.34277778455352", + "=GAMMA(5.5)": "52.3427777845535", // GAMMA.DIST "=GAMMA.DIST(6,3,2,FALSE)": "0.112020903827694", "=GAMMA.DIST(6,3,2,TRUE)": "0.576809918873156", @@ -861,10 +861,10 @@ func TestCalcCellValue(t *testing.T) { "=GAMMADIST(6,3,2,FALSE)": "0.112020903827694", "=GAMMADIST(6,3,2,TRUE)": "0.576809918873156", // GAMMA.INV - "=GAMMA.INV(0.5,3,2)": "5.348120627447122", + "=GAMMA.INV(0.5,3,2)": "5.34812062744712", "=GAMMA.INV(0.5,0.5,1)": "0.227468211559786", // GAMMAINV - "=GAMMAINV(0.5,3,2)": "5.348120627447122", + "=GAMMAINV(0.5,3,2)": "5.34812062744712", "=GAMMAINV(0.5,0.5,1)": "0.227468211559786", // GAMMALN "=GAMMALN(4.5)": "2.45373657084244", @@ -925,11 +925,11 @@ func TestCalcCellValue(t *testing.T) { "=NORMDIST(0.8,1,0.3,TRUE)": "0.252492537546923", "=NORMDIST(50,40,20,FALSE)": "0.017603266338215", // NORM.INV - "=NORM.INV(0.6,5,2)": "5.506694205719997", + "=NORM.INV(0.6,5,2)": "5.50669420572", // NORMINV - "=NORMINV(0.6,5,2)": "5.506694205719997", - "=NORMINV(0.99,40,1.5)": "43.489521811582044", - "=NORMINV(0.02,40,1.5)": "36.91937663649545", + "=NORMINV(0.6,5,2)": "5.50669420572", + "=NORMINV(0.99,40,1.5)": "43.489521811582", + "=NORMINV(0.02,40,1.5)": "36.9193766364954", // NORM.S.DIST "=NORM.S.DIST(0.8,TRUE)": "0.788144601416603", // NORMSDIST @@ -1052,7 +1052,7 @@ func TestCalcCellValue(t *testing.T) { "=TRIMMEAN(A1:B4,10%)": "2.5", "=TRIMMEAN(A1:B4,70%)": "2.5", // VAR - "=VAR(1,3,5,0,C1)": "4.916666666666667", + "=VAR(1,3,5,0,C1)": "4.91666666666667", "=VAR(1,3,5,0,C1,TRUE)": "4", // VARA "=VARA(1,3,5,0,C1)": "4.7", @@ -1063,16 +1063,16 @@ func TestCalcCellValue(t *testing.T) { // VAR.P "=VAR.P(A1:A5)": "1.25", // VAR.S - "=VAR.S(1,3,5,0,C1)": "4.916666666666667", + "=VAR.S(1,3,5,0,C1)": "4.91666666666667", "=VAR.S(1,3,5,0,C1,TRUE)": "4", // VARPA "=VARPA(1,3,5,0,C1)": "3.76", "=VARPA(1,3,5,0,C1,TRUE)": "3.22222222222222", // WEIBULL - "=WEIBULL(1,3,1,FALSE)": "1.103638323514327", + "=WEIBULL(1,3,1,FALSE)": "1.10363832351433", "=WEIBULL(2,5,1.5,TRUE)": "0.985212776817482", // WEIBULL.DIST - "=WEIBULL.DIST(1,3,1,FALSE)": "1.103638323514327", + "=WEIBULL.DIST(1,3,1,FALSE)": "1.10363832351433", "=WEIBULL.DIST(2,5,1.5,TRUE)": "0.985212776817482", // Information Functions // ERROR.TYPE @@ -1286,7 +1286,7 @@ func TestCalcCellValue(t *testing.T) { "=YEARFRAC(\"01/31/2015\",\"03/31/2015\")": "0.166666666666667", "=YEARFRAC(\"01/30/2015\",\"03/31/2015\")": "0.166666666666667", "=YEARFRAC(\"02/29/2000\", \"02/29/2008\")": "8", - "=YEARFRAC(\"02/29/2000\", \"02/29/2008\",1)": "7.998175182481752", + "=YEARFRAC(\"02/29/2000\", \"02/29/2008\",1)": "7.99817518248175", "=YEARFRAC(\"02/29/2000\", \"01/29/2001\",1)": "0.915300546448087", "=YEARFRAC(\"02/29/2000\", \"03/29/2000\",1)": "0.0792349726775956", "=YEARFRAC(\"01/31/2000\", \"03/29/2000\",4)": "0.163888888888889", @@ -1472,7 +1472,7 @@ func TestCalcCellValue(t *testing.T) { "=VALUE(\"5,000\")": "5000", "=VALUE(\"20%\")": "0.2", "=VALUE(\"12:00:00\")": "0.5", - "=VALUE(\"01/02/2006 15:04:05\")": "38719.62783564815", + "=VALUE(\"01/02/2006 15:04:05\")": "38719.6278356481", // Conditional Functions // IF "=IF(1=1)": "TRUE", @@ -1627,16 +1627,16 @@ func TestCalcCellValue(t *testing.T) { "=COUPPCD(\"10/25/2011\",\"01/01/2012\",4)": "40817", // CUMIPMT "=CUMIPMT(0.05/12,60,50000,1,12,0)": "-2294.97753732664", - "=CUMIPMT(0.05/12,60,50000,13,24,0)": "-1833.1000665738893", + "=CUMIPMT(0.05/12,60,50000,13,24,0)": "-1833.10006657389", // CUMPRINC - "=CUMPRINC(0.05/12,60,50000,1,12,0)": "-9027.762649079885", - "=CUMPRINC(0.05/12,60,50000,13,24,0)": "-9489.640119832635", + "=CUMPRINC(0.05/12,60,50000,1,12,0)": "-9027.76264907988", + "=CUMPRINC(0.05/12,60,50000,13,24,0)": "-9489.64011983263", // DB "=DB(0,1000,5,1)": "0", "=DB(10000,1000,5,1)": "3690", "=DB(10000,1000,5,2)": "2328.39", "=DB(10000,1000,5,1,6)": "1845", - "=DB(10000,1000,5,6,6)": "238.52712458788187", + "=DB(10000,1000,5,6,6)": "238.527124587882", // DDB "=DDB(0,1000,5,1)": "0", "=DDB(10000,1000,5,1)": "4000", @@ -1651,13 +1651,13 @@ func TestCalcCellValue(t *testing.T) { // DOLLARFR "=DOLLARFR(1.0625,16)": "1.01", // DURATION - "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4)": "6.674422798483131", + "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4)": "6.67442279848313", // EFFECT "=EFFECT(0.1,4)": "0.103812890625", "=EFFECT(0.025,2)": "0.02515625", // FV - "=FV(0.05/12,60,-1000)": "68006.08284084337", - "=FV(0.1/4,16,-2000,0,1)": "39729.46089416617", + "=FV(0.05/12,60,-1000)": "68006.0828408434", + "=FV(0.1/4,16,-2000,0,1)": "39729.4608941662", "=FV(0,16,-2000)": "32000", // FVSCHEDULE "=FVSCHEDULE(10000,A1:A5)": "240000", @@ -1665,60 +1665,60 @@ func TestCalcCellValue(t *testing.T) { // INTRATE "=INTRATE(\"04/01/2005\",\"03/31/2010\",1000,2125)": "0.225", // IPMT - "=IPMT(0.05/12,2,60,50000)": "-205.26988187971995", - "=IPMT(0.035/4,2,8,0,5000,1)": "5.257455237829077", + "=IPMT(0.05/12,2,60,50000)": "-205.26988187972", + "=IPMT(0.035/4,2,8,0,5000,1)": "5.25745523782908", // ISPMT - "=ISPMT(0.05/12,1,60,50000)": "-204.8611111111111", - "=ISPMT(0.05/12,2,60,50000)": "-201.38888888888886", - "=ISPMT(0.05/12,2,1,50000)": "208.33333333333334", + "=ISPMT(0.05/12,1,60,50000)": "-204.861111111111", + "=ISPMT(0.05/12,2,60,50000)": "-201.388888888889", + "=ISPMT(0.05/12,2,1,50000)": "208.333333333333", // MDURATION - "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4)": "6.543551763218756", + "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4)": "6.54355176321876", // NOMINAL "=NOMINAL(0.025,12)": "0.0247180352381129", // NPER - "=NPER(0.04,-6000,50000)": "10.338035071507665", - "=NPER(0,-6000,50000)": "8.333333333333334", - "=NPER(0.06/4,-2000,60000,30000,1)": "52.794773709274764", + "=NPER(0.04,-6000,50000)": "10.3380350715077", + "=NPER(0,-6000,50000)": "8.33333333333333", + "=NPER(0.06/4,-2000,60000,30000,1)": "52.7947737092748", // NPV - "=NPV(0.02,-5000,\"\",800)": "-4133.025759323337", + "=NPV(0.02,-5000,\"\",800)": "-4133.02575932334", // ODDFPRICE - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": "107.69183025662932", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,1)": "106.76691501092883", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,3)": "106.7819138146997", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,4)": "106.77191377246672", - "=ODDFPRICE(\"11/11/2008\",\"03/01/2021\",\"10/15/2008\",\"03/01/2009\",7.85%,6.25%,100,2,1)": "113.59771747407883", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"09/30/2017\",5.5%,3.5%,100,4,0)": "106.72930611878041", - "=ODDFPRICE(\"11/11/2008\",\"03/29/2021\", \"08/15/2008\", \"03/29/2009\", 0.0785, 0.0625, 100, 2, 1)": "113.61826640813996", + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": "107.691830256629", + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,1)": "106.766915010929", + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,3)": "106.7819138147", + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,4)": "106.771913772467", + "=ODDFPRICE(\"11/11/2008\",\"03/01/2021\",\"10/15/2008\",\"03/01/2009\",7.85%,6.25%,100,2,1)": "113.597717474079", + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"09/30/2017\",5.5%,3.5%,100,4,0)": "106.72930611878", + "=ODDFPRICE(\"11/11/2008\",\"03/29/2021\", \"08/15/2008\", \"03/29/2009\", 0.0785, 0.0625, 100, 2, 1)": "113.61826640814", // PDURATION - "=PDURATION(0.04,10000,15000)": "10.33803507150765", + "=PDURATION(0.04,10000,15000)": "10.3380350715076", // PMT "=PMT(0,8,0,5000,1)": "-625", - "=PMT(0.035/4,8,0,5000,1)": "-600.8520271804658", + "=PMT(0.035/4,8,0,5000,1)": "-600.852027180466", // PRICE - "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2)": "110.65510517844305", - "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2,4)": "110.65510517844305", - "=PRICE(\"04/01/2012\",\"03/31/2020\",12%,10%,100,2)": "110.83448359321572", - "=PRICE(\"01/01/2010\",\"06/30/2010\",0.5,1,1,1,4)": "8.924190888476605", + "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2)": "110.655105178443", + "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2,4)": "110.655105178443", + "=PRICE(\"04/01/2012\",\"03/31/2020\",12%,10%,100,2)": "110.834483593216", + "=PRICE(\"01/01/2010\",\"06/30/2010\",0.5,1,1,1,4)": "8.92419088847661", // PPMT - "=PPMT(0.05/12,2,60,50000)": "-738.2918003208238", - "=PPMT(0.035/4,2,8,0,5000,1)": "-606.1094824182949", + "=PPMT(0.05/12,2,60,50000)": "-738.291800320824", + "=PPMT(0.035/4,2,8,0,5000,1)": "-606.109482418295", // PRICEDISC "=PRICEDISC(\"04/01/2017\",\"03/31/2021\",2.5%,100)": "90", "=PRICEDISC(\"04/01/2017\",\"03/31/2021\",2.5%,100,3)": "90", // PRICEMAT - "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%)": "107.17045454545453", - "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%,0)": "107.17045454545453", + "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%)": "107.170454545455", + "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%,0)": "107.170454545455", // PV "=PV(0,60,1000)": "-60000", - "=PV(5%/12,60,1000)": "-52990.70632392748", - "=PV(10%/4,16,2000,0,1)": "-26762.75545288113", + "=PV(5%/12,60,1000)": "-52990.7063239275", + "=PV(10%/4,16,2000,0,1)": "-26762.7554528811", // RATE "=RATE(60,-1000,50000)": "0.0061834131621292", "=RATE(24,-800,0,20000,1)": "0.00325084350160374", "=RATE(48,-200,8000,3,1,0.5)": "0.0080412665831637", // RECEIVED - "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%)": "1290.3225806451612", - "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,0)": "1290.3225806451612", + "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%)": "1290.32258064516", + "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,0)": "1290.32258064516", // RRI "=RRI(10,10000,15000)": "0.0413797439924106", // SLN @@ -1729,7 +1729,7 @@ func TestCalcCellValue(t *testing.T) { // TBILLEQ "=TBILLEQ(\"01/01/2017\",\"06/30/2017\",2.5%)": "0.0256680731364276", // TBILLPRICE - "=TBILLPRICE(\"02/01/2017\",\"06/30/2017\",2.75%)": "98.86180555555556", + "=TBILLPRICE(\"02/01/2017\",\"06/30/2017\",2.75%)": "98.8618055555556", // TBILLYIELD "=TBILLYIELD(\"02/01/2017\",\"06/30/2017\",99)": "0.024405125076266", // VDB @@ -1739,10 +1739,10 @@ func TestCalcCellValue(t *testing.T) { "=VDB(10000,1000,5,3,5,0.2,FALSE)": "3600", "=VDB(10000,1000,5,3,5,0.2,TRUE)": "693.633024", "=VDB(24000,3000,10,0,0.875,2)": "4200", - "=VDB(24000,3000,10,0.1,1)": "4233.599999999999", - "=VDB(24000,3000,10,0.1,1,1)": "2138.3999999999996", - "=VDB(24000,3000,100,50,100,1)": "10377.294418465235", - "=VDB(24000,3000,100,50,100,2)": "5740.072322090805", + "=VDB(24000,3000,10,0.1,1)": "4233.6", + "=VDB(24000,3000,10,0.1,1,1)": "2138.4", + "=VDB(24000,3000,100,50,100,1)": "10377.2944184652", + "=VDB(24000,3000,100,50,100,2)": "5740.0723220908", // YIELD "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4)": "0.0975631546829798", "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4,4)": "0.0976269355643988", @@ -4284,7 +4284,7 @@ func TestCalcXNPV(t *testing.T) { } f := prepareCalcData(cellData) formulaList := map[string]string{ - "=XNPV(B1,B2:B7,A2:A7)": "4447.938009440515", + "=XNPV(B1,B2:B7,A2:A7)": "4447.93800944052", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) diff --git a/cell.go b/cell.go index c3b62dc212..6ecce4febe 100644 --- a/cell.go +++ b/cell.go @@ -1088,22 +1088,12 @@ func (f *File) formattedValue(s int, v string, raw bool) string { if raw { return v } - precise := v - isNum, precision := isNumeric(v) - if isNum { - if precision > 15 { - precise = roundPrecision(v, 15) - } - if precision <= 15 { - precise = roundPrecision(v, -1) - } - } if s == 0 { - return precise + return v } styleSheet := f.stylesReader() if s >= len(styleSheet.CellXfs.Xf) { - return precise + return v } var numFmtID int if styleSheet.CellXfs.Xf[s].NumFmtID != nil { @@ -1112,17 +1102,17 @@ func (f *File) formattedValue(s int, v string, raw bool) string { ok := builtInNumFmtFunc[numFmtID] if ok != nil { - return ok(precise, builtInNumFmt[numFmtID]) + return ok(v, builtInNumFmt[numFmtID]) } if styleSheet == nil || styleSheet.NumFmts == nil { - return precise + return v } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { - return format(precise, xlsxFmt.FormatCode) + return format(v, xlsxFmt.FormatCode) } } - return precise + return v } // prepareCellStyle provides a function to prepare style index of cell in diff --git a/file.go b/file.go index 0cfed05ef8..0135e20eaa 100644 --- a/file.go +++ b/file.go @@ -81,7 +81,7 @@ func (f *File) SaveAs(name string, opt ...Options) error { return ErrWorkbookExt } f.setContentTypePartProjectExtensions(contentType) - file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600) + file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o600) if err != nil { return err } diff --git a/lib.go b/lib.go index 5bbbec9cae..439e50a82a 100644 --- a/lib.go +++ b/lib.go @@ -688,12 +688,15 @@ func isNumeric(s string) (bool, int) { if i == 0 && v == '-' { continue } - if e && (v == '+' || v == '-') { + if e && v == '-' { + return true, 0 + } + if e && v == '+' { p = 15 continue } return false, 0 - } else if dot { + } else { p++ } n = true diff --git a/lib_test.go b/lib_test.go index 1e2f3249c8..027e5dd6b6 100644 --- a/lib_test.go +++ b/lib_test.go @@ -342,7 +342,7 @@ func TestReadBytes(t *testing.T) { func TestUnzipToTemp(t *testing.T) { os.Setenv("TMPDIR", "test") defer os.Unsetenv("TMPDIR") - assert.NoError(t, os.Chmod(os.TempDir(), 0444)) + assert.NoError(t, os.Chmod(os.TempDir(), 0o444)) f := NewFile() data := []byte("PK\x03\x040000000PK\x01\x0200000" + "0000000000000000000\x00" + @@ -364,7 +364,7 @@ func TestUnzipToTemp(t *testing.T) { _, err = f.unzipToTemp(z.File[0]) require.Error(t, err) - assert.NoError(t, os.Chmod(os.TempDir(), 0755)) + assert.NoError(t, os.Chmod(os.TempDir(), 0o755)) _, err = f.unzipToTemp(z.File[0]) assert.EqualError(t, err, "EOF") diff --git a/numfmt.go b/numfmt.go index 50ce1f313b..3b20e028d3 100644 --- a/numfmt.go +++ b/numfmt.go @@ -13,6 +13,7 @@ package excelize import ( "fmt" + "math" "strconv" "strings" "time" @@ -41,13 +42,15 @@ type numberFormat struct { var ( // supportedTokenTypes list the supported number format token types currently. supportedTokenTypes = []string{ + nfp.TokenSubTypeLanguageInfo, + nfp.TokenTypeColor, nfp.TokenTypeCurrencyLanguage, nfp.TokenTypeDateTimes, nfp.TokenTypeElapsedDateTimes, nfp.TokenTypeGeneral, nfp.TokenTypeLiteral, nfp.TokenTypeTextPlaceHolder, - nfp.TokenSubTypeLanguageInfo, + nfp.TokenTypeZeroPlaceHolder, } // supportedLanguageInfo directly maps the supported language ID and tags. supportedLanguageInfo = map[string]languageInfo{ @@ -276,11 +279,9 @@ var ( // prepareNumberic split the number into two before and after parts by a // decimal point. func (nf *numberFormat) prepareNumberic(value string) { - prec := 0 - if nf.isNumberic, prec = isNumeric(value); !nf.isNumberic { + if nf.isNumberic, _ = isNumeric(value); !nf.isNumberic { return } - nf.beforePoint, nf.afterPoint = value[:len(value)-prec-1], value[len(value)-prec:] } // format provides a function to return a string parse by number format @@ -336,6 +337,20 @@ func (nf *numberFormat) positiveHandler() (result string) { nf.result += token.TValue continue } + if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == "0" { + if isNum, precision := isNumeric(nf.value); isNum { + if nf.number < 1 { + nf.result += "0" + continue + } + if precision > 15 { + nf.result += roundPrecision(nf.value, 15) + } else { + nf.result += fmt.Sprintf("%.f", nf.number) + } + continue + } + } } result = nf.result return @@ -874,8 +889,33 @@ func (nf *numberFormat) secondsNext(i int) bool { // negativeHandler will be handling negative selection for a number format // expression. -func (nf *numberFormat) negativeHandler() string { - return nf.value +func (nf *numberFormat) negativeHandler() (result string) { + for _, token := range nf.section[nf.sectionIdx].Items { + if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { + result = nf.value + return + } + if token.TType == nfp.TokenTypeLiteral { + nf.result += token.TValue + continue + } + if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == "0" { + if isNum, precision := isNumeric(nf.value); isNum { + if math.Abs(nf.number) < 1 { + nf.result += "0" + continue + } + if precision > 15 { + nf.result += strings.TrimLeft(roundPrecision(nf.value, 15), "-") + } else { + nf.result += fmt.Sprintf("%.f", math.Abs(nf.number)) + } + continue + } + } + } + result = nf.result + return } // zeroHandler will be handling zero selection for a number format expression. diff --git a/numfmt_test.go b/numfmt_test.go index 80534e97e7..7dc3f770b2 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -996,6 +996,14 @@ func TestNumFmt(t *testing.T) { {"44896.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "D 01 2022 4:32 AM"}, {"text_", "General", "text_"}, {"text_", "\"=====\"@@@\"--\"@\"----\"", "=====text_text_text_--text_----"}, + {"0.0450685976001E+21", "0_);[Red]\\(0\\)", "45068597600100000000"}, + {"8.0450685976001E+21", "0_);[Red]\\(0\\)", "8045068597600100000000"}, + {"8.0450685976001E-21", "0_);[Red]\\(0\\)", "0"}, + {"8.04506", "0_);[Red]\\(0\\)", "8"}, + {"-0.0450685976001E+21", "0_);[Red]\\(0\\)", "(45068597600100000000)"}, + {"-8.0450685976001E+21", "0_);[Red]\\(0\\)", "(8045068597600100000000)"}, + {"-8.0450685976001E-21", "0_);[Red]\\(0\\)", "(0)"}, + {"-8.04506", "0_);[Red]\\(0\\)", "(8)"}, } { result := format(item[0], item[1]) assert.Equal(t, item[2], result, item) diff --git a/picture_test.go b/picture_test.go index 8da7c3d870..fbbdf114b5 100644 --- a/picture_test.go +++ b/picture_test.go @@ -102,7 +102,7 @@ func TestGetPicture(t *testing.T) { file, raw, err := f.GetPicture("Sheet1", "F21") assert.NoError(t, err) if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) || - !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0644)) { + !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0o644)) { t.FailNow() } @@ -137,7 +137,7 @@ func TestGetPicture(t *testing.T) { file, raw, err = f.GetPicture("Sheet1", "F21") assert.NoError(t, err) if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) || - !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0644)) { + !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0o644)) { t.FailNow() } diff --git a/rows.go b/rows.go index ec94c644ec..ae7e01ed7c 100644 --- a/rows.go +++ b/rows.go @@ -459,6 +459,13 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { } return f.formattedValue(c.S, c.V, raw), nil default: + if isNum, precision := isNumeric(c.V); isNum && !raw { + if precision == 0 { + c.V = roundPrecision(c.V, 15) + } else { + c.V = roundPrecision(c.V, -1) + } + } return f.formattedValue(c.S, c.V, raw), nil } } From 49424b0eb3e35201fd7f922a1ad80b6c4d4976c4 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 20 Mar 2022 00:16:32 +0800 Subject: [PATCH 007/213] ref #65, #1185, new formula functions and precision improvement * New formula functions: BETA.DIST, BINOMDIST and BINOM * Fix a part of formula function calculation result precision issue on arm64 --- calc.go | 142 +++++++++++++++++++++++++++++++++++++++++++++++++-- calc_test.go | 65 ++++++++++++++++++++--- 2 files changed, 197 insertions(+), 10 deletions(-) diff --git a/calc.go b/calc.go index 9dc430d3d2..44633734d2 100644 --- a/calc.go +++ b/calc.go @@ -334,11 +334,14 @@ type formulaFuncs struct { // BESSELK // BESSELY // BETADIST +// BETA.DIST // BETAINV // BETA.INV // BIN2DEC // BIN2HEX // BIN2OCT +// BINOMDIST +// BINOM.DIST // BITAND // BITLSHIFT // BITOR @@ -686,7 +689,7 @@ func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { } result = token.TValue isNum, precision := isNumeric(result) - if isNum && precision > 15 { + if isNum && (precision > 15 || precision == 0) { num := roundPrecision(result, -1) result = strings.ToUpper(num) } @@ -5406,6 +5409,74 @@ func getBetaDist(fXin, fAlpha, fBeta float64) float64 { return fResult } +// prepareBETAdotDISTArgs checking and prepare arguments for the formula +// function BETA.DIST. +func (fn *formulaFuncs) prepareBETAdotDISTArgs(argsList *list.List) formulaArg { + if argsList.Len() < 4 { + return newErrorFormulaArg(formulaErrorVALUE, "BETA.DIST requires at least 4 arguments") + } + if argsList.Len() > 6 { + return newErrorFormulaArg(formulaErrorVALUE, "BETA.DIST requires at most 6 arguments") + } + x := argsList.Front().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + alpha := argsList.Front().Next().Value.(formulaArg).ToNumber() + if alpha.Type != ArgNumber { + return alpha + } + beta := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if beta.Type != ArgNumber { + return beta + } + if alpha.Number <= 0 || beta.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + cumulative := argsList.Front().Next().Next().Next().Value.(formulaArg).ToBool() + if cumulative.Type != ArgNumber { + return cumulative + } + a, b := newNumberFormulaArg(0), newNumberFormulaArg(1) + if argsList.Len() > 4 { + if a = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); a.Type != ArgNumber { + return a + } + } + if argsList.Len() == 6 { + if b = argsList.Back().Value.(formulaArg).ToNumber(); b.Type != ArgNumber { + return b + } + } + return newListFormulaArg([]formulaArg{x, alpha, beta, cumulative, a, b}) +} + +// BETAdotDIST function calculates the cumulative beta distribution function +// or the probability density function of the Beta distribution, for a +// supplied set of parameters. The syntax of the function is: +// +// BETA.DIST(x,alpha,beta,cumulative,[A],[B]) +// +func (fn *formulaFuncs) BETAdotDIST(argsList *list.List) formulaArg { + args := fn.prepareBETAdotDISTArgs(argsList) + if args.Type != ArgList { + return args + } + x, alpha, beta, cumulative, a, b := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5] + if x.Number < a.Number || x.Number > b.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if a.Number == b.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + fScale := b.Number - a.Number + x.Number = (x.Number - a.Number) / fScale + if cumulative.Number == 1 { + return newNumberFormulaArg(getBetaDist(x.Number, alpha.Number, beta.Number)) + } + return newNumberFormulaArg(getBetaDistPDF(x.Number, alpha.Number, beta.Number) / fScale) +} + // BETADIST function calculates the cumulative beta probability density // function for a supplied set of parameters. The syntax of the function is: // @@ -5836,6 +5907,69 @@ func incompleteGamma(a, x float64) float64 { return math.Pow(x, a) * math.Exp(0-x) * summer } +// binomCoeff implement binomial coefficient calcuation. +func binomCoeff(n, k float64) float64 { + return fact(n) / (fact(k) * fact(n-k)) +} + +// binomdist implement binomial distribution calcuation. +func binomdist(x, n, p float64) float64 { + return binomCoeff(n, x) * math.Pow(p, x) * math.Pow(1-p, n-x) +} + +// BINOMfotDIST function returns the Binomial Distribution probability for a +// given number of successes from a specified number of trials. The syntax of +// the function is: +// +// BINOM.DIST(number_s,trials,probability_s,cumulative) +// +func (fn *formulaFuncs) BINOMdotDIST(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "BINOM.DIST requires 4 arguments") + } + return fn.BINOMDIST(argsList) +} + +// BINOMDIST function returns the Binomial Distribution probability of a +// specified number of successes out of a specified number of trials. The +// syntax of the function is: +// +// BINOMDIST(number_s,trials,probability_s,cumulative) +// +func (fn *formulaFuncs) BINOMDIST(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "BINOMDIST requires 4 arguments") + } + var s, trials, probability, cumulative formulaArg + if s = argsList.Front().Value.(formulaArg).ToNumber(); s.Type != ArgNumber { + return s + } + if trials = argsList.Front().Next().Value.(formulaArg).ToNumber(); trials.Type != ArgNumber { + return trials + } + if s.Number < 0 || s.Number > trials.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if probability = argsList.Back().Prev().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { + return probability + } + + if probability.Number < 0 || probability.Number > 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError { + return cumulative + } + if cumulative.Number == 1 { + bm := 0.0 + for i := 0; i <= int(s.Number); i++ { + bm += binomdist(float64(i), trials.Number, probability.Number) + } + return newNumberFormulaArg(bm) + } + return newNumberFormulaArg(binomdist(s.Number, trials.Number, probability.Number)) +} + // CHIDIST function calculates the right-tailed probability of the chi-square // distribution. The syntax of the function is: // @@ -11641,7 +11775,7 @@ func (fn *formulaFuncs) AMORLINC(argsList *list.List) formulaArg { if int(period.Number) <= periods { return newNumberFormulaArg(rate2) } else if int(period.Number)-1 == periods { - return newNumberFormulaArg(delta - rate2*float64(periods) - rate1) + return newNumberFormulaArg(delta - rate2*float64(periods) - math.Nextafter(rate1, rate1)) } return newNumberFormulaArg(0) } @@ -13334,7 +13468,9 @@ func (fn *formulaFuncs) rate(nper, pmt, pv, fv, t, guess formulaArg, argsList *l rt := rate*t.Number + 1 p0 := pmt.Number * (t1 - 1) f1 := fv.Number + t1*pv.Number + p0*rt/rate - f2 := nper.Number*t2*pv.Number - p0*rt/math.Pow(rate, 2) + n1 := nper.Number * t2 * pv.Number + n2 := p0 * rt / math.Pow(rate, 2) + f2 := math.Nextafter(n1, n1) - math.Nextafter(n2, n2) f3 := (nper.Number*pmt.Number*t2*rt + p0*t.Number) / rate delta := f1 / (f2 + f3) if math.Abs(delta) < epsMax { diff --git a/calc_test.go b/calc_test.go index d350037926..e0cd0d0e9a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -784,6 +784,9 @@ func TestCalcCellValue(t *testing.T) { "=AVERAGEA(A1)": "1", "=AVERAGEA(A1:A2)": "1.5", "=AVERAGEA(D2:F9)": "12671.375", + // BETA.DIST + "=BETA.DIST(0.4,4,5,TRUE,0,1)": "0.4059136", + "=BETA.DIST(0.6,4,5,FALSE,0,1)": "1.548288", // BETADIST "=BETADIST(0.4,4,5)": "0.4059136", "=BETADIST(0.4,4,5,0,1)": "0.4059136", @@ -796,12 +799,26 @@ func TestCalcCellValue(t *testing.T) { "=BETADIST(0.4,2,100)": "1", "=BETADIST(0.75,3,4)": "0.96240234375", "=BETADIST(0.2,0.7,4)": "0.71794309318323", - "=BETADIST(0.01,3,4)": "1.9553589999999998e-05", + "=BETADIST(0.01,3,4)": "1.955359E-05", "=BETADIST(0.75,130,140)": "1", // BETAINV "=BETAINV(0.2,4,5,0,1)": "0.303225844664082", // BETA.INV "=BETA.INV(0.2,4,5,0,1)": "0.303225844664082", + // BINOMDIST + "=BINOMDIST(10,100,0.5,FALSE)": "1.36554263874631E-17", + "=BINOMDIST(50,100,0.5,FALSE)": "0.0795892373871787", + "=BINOMDIST(65,100,0.5,FALSE)": "0.000863855665741652", + "=BINOMDIST(10,100,0.5,TRUE)": "1.53164508771899E-17", + "=BINOMDIST(50,100,0.5,TRUE)": "0.539794618693589", + "=BINOMDIST(65,100,0.5,TRUE)": "0.999105034804256", + // BINOM.DIST + "=BINOM.DIST(10,100,0.5,FALSE)": "1.36554263874631E-17", + "=BINOM.DIST(50,100,0.5,FALSE)": "0.0795892373871787", + "=BINOM.DIST(65,100,0.5,FALSE)": "0.000863855665741652", + "=BINOM.DIST(10,100,0.5,TRUE)": "1.53164508771899E-17", + "=BINOM.DIST(50,100,0.5,TRUE)": "0.539794618693589", + "=BINOM.DIST(65,100,0.5,TRUE)": "0.999105034804256", // CHIDIST "=CHIDIST(0.5,3)": "0.918891411654676", "=CHIDIST(8,3)": "0.0460117056892315", @@ -1468,7 +1485,7 @@ func TestCalcCellValue(t *testing.T) { "=UPPER(\"TEST 123\")": "TEST 123", // VALUE "=VALUE(\"50\")": "50", - "=VALUE(\"1.0E-07\")": "1e-07", + "=VALUE(\"1.0E-07\")": "1E-07", "=VALUE(\"5,000\")": "5000", "=VALUE(\"20%\")": "0.2", "=VALUE(\"12:00:00\")": "0.5", @@ -2341,6 +2358,25 @@ func TestCalcCellValue(t *testing.T) { "=AVERAGE(H1)": "AVERAGE divide by zero", // AVERAGEA "=AVERAGEA(H1)": "AVERAGEA divide by zero", + // AVERAGEIF + "=AVERAGEIF()": "AVERAGEIF requires at least 2 arguments", + "=AVERAGEIF(H1,\"\")": "AVERAGEIF divide by zero", + "=AVERAGEIF(D1:D3,\"Month\",D1:D3)": "AVERAGEIF divide by zero", + "=AVERAGEIF(C1:C3,\"Month\",D1:D3)": "AVERAGEIF divide by zero", + // BETA.DIST + "=BETA.DIST()": "BETA.DIST requires at least 4 arguments", + "=BETA.DIST(0.4,4,5,TRUE,0,1,0)": "BETA.DIST requires at most 6 arguments", + "=BETA.DIST(\"\",4,5,TRUE,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BETA.DIST(0.4,\"\",5,TRUE,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BETA.DIST(0.4,4,\"\",TRUE,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BETA.DIST(0.4,4,5,\"\",0,1)": "strconv.ParseBool: parsing \"\": invalid syntax", + "=BETA.DIST(0.4,4,5,TRUE,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BETA.DIST(0.4,4,5,TRUE,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BETA.DIST(0.4,0,5,TRUE,0,1)": "#NUM!", + "=BETA.DIST(0.4,4,0,TRUE,0,0)": "#NUM!", + "=BETA.DIST(0.4,4,5,TRUE,0.5,1)": "#NUM!", + "=BETA.DIST(0.4,4,5,TRUE,0,0.3)": "#NUM!", + "=BETA.DIST(0.4,4,5,TRUE,0.4,0.4)": "#NUM!", // BETADIST "=BETADIST()": "BETADIST requires at least 3 arguments", "=BETADIST(0.4,4,5,0,1,0)": "BETADIST requires at most 5 arguments", @@ -2380,11 +2416,26 @@ func TestCalcCellValue(t *testing.T) { "=BETA.INV(0.2,0,5,0,1)": "#NUM!", "=BETA.INV(0.2,4,0,0,1)": "#NUM!", "=BETA.INV(0.2,4,5,2,2)": "#NUM!", - // AVERAGEIF - "=AVERAGEIF()": "AVERAGEIF requires at least 2 arguments", - "=AVERAGEIF(H1,\"\")": "AVERAGEIF divide by zero", - "=AVERAGEIF(D1:D3,\"Month\",D1:D3)": "AVERAGEIF divide by zero", - "=AVERAGEIF(C1:C3,\"Month\",D1:D3)": "AVERAGEIF divide by zero", + // BINOMDIST + "=BINOMDIST()": "BINOMDIST requires 4 arguments", + "=BINOMDIST(\"\",100,0.5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOMDIST(10,\"\",0.5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOMDIST(10,100,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOMDIST(10,100,0.5,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=BINOMDIST(-1,100,0.5,FALSE)": "#NUM!", + "=BINOMDIST(110,100,0.5,FALSE)": "#NUM!", + "=BINOMDIST(10,100,-1,FALSE)": "#NUM!", + "=BINOMDIST(10,100,2,FALSE)": "#NUM!", + // BINOM.DIST + "=BINOM.DIST()": "BINOM.DIST requires 4 arguments", + "=BINOM.DIST(\"\",100,0.5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOM.DIST(10,\"\",0.5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOM.DIST(10,100,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOM.DIST(10,100,0.5,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=BINOM.DIST(-1,100,0.5,FALSE)": "#NUM!", + "=BINOM.DIST(110,100,0.5,FALSE)": "#NUM!", + "=BINOM.DIST(10,100,-1,FALSE)": "#NUM!", + "=BINOM.DIST(10,100,2,FALSE)": "#NUM!", // CHIDIST "=CHIDIST()": "CHIDIST requires 2 numeric arguments", "=CHIDIST(\"\",3)": "strconv.ParseFloat: parsing \"\": invalid syntax", From 067c5d564383aa56f91fc0b9250352a5542c1e6f Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 21 Mar 2022 00:02:42 +0800 Subject: [PATCH 008/213] This closes #1185, fix formula function calculation result precision issue on arm64 * New formula functions: BINOM.DIST.RANGE and BINOM.INV * Fix complex number calculation result precision issue --- calc.go | 257 ++++++++++++++++++++++++++++++++++++++++----------- calc_test.go | 123 +++++++++++++++--------- 2 files changed, 282 insertions(+), 98 deletions(-) diff --git a/calc.go b/calc.go index 44633734d2..38be45cf7f 100644 --- a/calc.go +++ b/calc.go @@ -342,6 +342,8 @@ type formulaFuncs struct { // BIN2OCT // BINOMDIST // BINOM.DIST +// BINOM.DIST.RANGE +// BINOM.INV // BITAND // BITLSHIFT // BITOR @@ -1985,13 +1987,27 @@ func (fn *formulaFuncs) COMPLEX(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } } - return newStringFormulaArg(cmplx2str(fmt.Sprint(complex(real.Number, i.Number)), suffix)) + return newStringFormulaArg(cmplx2str(complex(real.Number, i.Number), suffix)) } // cmplx2str replace complex number string characters. -func cmplx2str(c, suffix string) string { - if c == "(0+0i)" || c == "(-0+0i)" || c == "(0-0i)" || c == "(-0-0i)" { - return "0" +func cmplx2str(num complex128, suffix string) string { + c := fmt.Sprint(num) + realPart, imagPart := fmt.Sprint(real(num)), fmt.Sprint(imag(num)) + isNum, i := isNumeric(realPart) + if isNum && i > 15 { + realPart = roundPrecision(realPart, -1) + } + isNum, i = isNumeric(imagPart) + if isNum && i > 15 { + imagPart = roundPrecision(imagPart, -1) + } + c = realPart + if imag(num) > 0 { + c += "+" + } + if imag(num) != 0 { + c += imagPart + "i" } c = strings.TrimPrefix(c, "(") c = strings.TrimPrefix(c, "+0+") @@ -2325,7 +2341,8 @@ func (fn *formulaFuncs) IMABS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMABS requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } @@ -2341,7 +2358,8 @@ func (fn *formulaFuncs) IMAGINARY(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMAGINARY requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } @@ -2357,7 +2375,8 @@ func (fn *formulaFuncs) IMARGUMENT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMARGUMENT requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } @@ -2373,11 +2392,12 @@ func (fn *formulaFuncs) IMCONJUGATE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCONJUGATE requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Conj(inumber)), "i")) + return newStringFormulaArg(cmplx2str(cmplx.Conj(inumber), value[len(value)-1:])) } // IMCOS function returns the cosine of a supplied complex number. The syntax @@ -2389,11 +2409,12 @@ func (fn *formulaFuncs) IMCOS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCOS requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Cos(inumber)), "i")) + return newStringFormulaArg(cmplx2str(cmplx.Cos(inumber), value[len(value)-1:])) } // IMCOSH function returns the hyperbolic cosine of a supplied complex number. The syntax @@ -2405,11 +2426,12 @@ func (fn *formulaFuncs) IMCOSH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCOSH requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Cosh(inumber)), "i")) + return newStringFormulaArg(cmplx2str(cmplx.Cosh(inumber), value[len(value)-1:])) } // IMCOT function returns the cotangent of a supplied complex number. The syntax @@ -2421,11 +2443,12 @@ func (fn *formulaFuncs) IMCOT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCOT requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Cot(inumber)), "i")) + return newStringFormulaArg(cmplx2str(cmplx.Cot(inumber), value[len(value)-1:])) } // IMCSC function returns the cosecant of a supplied complex number. The syntax @@ -2437,7 +2460,8 @@ func (fn *formulaFuncs) IMCSC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCSC requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } @@ -2445,7 +2469,7 @@ func (fn *formulaFuncs) IMCSC(argsList *list.List) formulaArg { if cmplx.IsInf(num) { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) + return newStringFormulaArg(cmplx2str(num, value[len(value)-1:])) } // IMCSCH function returns the hyperbolic cosecant of a supplied complex @@ -2457,7 +2481,8 @@ func (fn *formulaFuncs) IMCSCH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCSCH requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } @@ -2465,7 +2490,7 @@ func (fn *formulaFuncs) IMCSCH(argsList *list.List) formulaArg { if cmplx.IsInf(num) { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) + return newStringFormulaArg(cmplx2str(num, value[len(value)-1:])) } // IMDIV function calculates the quotient of two complex numbers (i.e. divides @@ -2477,7 +2502,8 @@ func (fn *formulaFuncs) IMDIV(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "IMDIV requires 2 arguments") } - inumber1, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber1, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } @@ -2489,7 +2515,7 @@ func (fn *formulaFuncs) IMDIV(argsList *list.List) formulaArg { if cmplx.IsInf(num) { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) + return newStringFormulaArg(cmplx2str(num, value[len(value)-1:])) } // IMEXP function returns the exponential of a supplied complex number. The @@ -2501,11 +2527,12 @@ func (fn *formulaFuncs) IMEXP(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMEXP requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Exp(inumber)), "i")) + return newStringFormulaArg(cmplx2str(cmplx.Exp(inumber), value[len(value)-1:])) } // IMLN function returns the natural logarithm of a supplied complex number. @@ -2517,7 +2544,8 @@ func (fn *formulaFuncs) IMLN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMLN requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } @@ -2525,7 +2553,7 @@ func (fn *formulaFuncs) IMLN(argsList *list.List) formulaArg { if cmplx.IsInf(num) { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) + return newStringFormulaArg(cmplx2str(num, value[len(value)-1:])) } // IMLOG10 function returns the common (base 10) logarithm of a supplied @@ -2537,7 +2565,8 @@ func (fn *formulaFuncs) IMLOG10(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMLOG10 requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } @@ -2545,7 +2574,7 @@ func (fn *formulaFuncs) IMLOG10(argsList *list.List) formulaArg { if cmplx.IsInf(num) { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) + return newStringFormulaArg(cmplx2str(num, value[len(value)-1:])) } // IMLOG2 function calculates the base 2 logarithm of a supplied complex @@ -2557,7 +2586,8 @@ func (fn *formulaFuncs) IMLOG2(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMLOG2 requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } @@ -2565,7 +2595,7 @@ func (fn *formulaFuncs) IMLOG2(argsList *list.List) formulaArg { if cmplx.IsInf(num) { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(num/cmplx.Log(2)), "i")) + return newStringFormulaArg(cmplx2str(num/cmplx.Log(2), value[len(value)-1:])) } // IMPOWER function returns a supplied complex number, raised to a given @@ -2577,7 +2607,8 @@ func (fn *formulaFuncs) IMPOWER(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "IMPOWER requires 2 arguments") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } @@ -2592,7 +2623,7 @@ func (fn *formulaFuncs) IMPOWER(argsList *list.List) formulaArg { if cmplx.IsInf(num) { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) + return newStringFormulaArg(cmplx2str(num, value[len(value)-1:])) } // IMPRODUCT function calculates the product of two or more complex numbers. @@ -2631,7 +2662,7 @@ func (fn *formulaFuncs) IMPRODUCT(argsList *list.List) formulaArg { } } } - return newStringFormulaArg(cmplx2str(fmt.Sprint(product), "i")) + return newStringFormulaArg(cmplx2str(product, "i")) } // IMREAL function returns the real coefficient of a supplied complex number. @@ -2643,11 +2674,12 @@ func (fn *formulaFuncs) IMREAL(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMREAL requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(real(inumber)), "i")) + return newStringFormulaArg(fmt.Sprint(real(inumber))) } // IMSEC function returns the secant of a supplied complex number. The syntax @@ -2659,11 +2691,12 @@ func (fn *formulaFuncs) IMSEC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMSEC requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(1/cmplx.Cos(inumber)), "i")) + return newStringFormulaArg(cmplx2str(1/cmplx.Cos(inumber), value[len(value)-1:])) } // IMSECH function returns the hyperbolic secant of a supplied complex number. @@ -2675,11 +2708,12 @@ func (fn *formulaFuncs) IMSECH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMSECH requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(1/cmplx.Cosh(inumber)), "i")) + return newStringFormulaArg(cmplx2str(1/cmplx.Cosh(inumber), value[len(value)-1:])) } // IMSIN function returns the Sine of a supplied complex number. The syntax of @@ -2691,11 +2725,12 @@ func (fn *formulaFuncs) IMSIN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMSIN requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Sin(inumber)), "i")) + return newStringFormulaArg(cmplx2str(cmplx.Sin(inumber), value[len(value)-1:])) } // IMSINH function returns the hyperbolic sine of a supplied complex number. @@ -2707,11 +2742,12 @@ func (fn *formulaFuncs) IMSINH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMSINH requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Sinh(inumber)), "i")) + return newStringFormulaArg(cmplx2str(cmplx.Sinh(inumber), value[len(value)-1:])) } // IMSQRT function returns the square root of a supplied complex number. The @@ -2723,11 +2759,12 @@ func (fn *formulaFuncs) IMSQRT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMSQRT requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Sqrt(inumber)), "i")) + return newStringFormulaArg(cmplx2str(cmplx.Sqrt(inumber), value[len(value)-1:])) } // IMSUB function calculates the difference between two complex numbers @@ -2748,7 +2785,7 @@ func (fn *formulaFuncs) IMSUB(argsList *list.List) formulaArg { if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(i1-i2), "i")) + return newStringFormulaArg(cmplx2str(i1-i2, "i")) } // IMSUM function calculates the sum of two or more complex numbers. The @@ -2769,7 +2806,7 @@ func (fn *formulaFuncs) IMSUM(argsList *list.List) formulaArg { } result += num } - return newStringFormulaArg(cmplx2str(fmt.Sprint(result), "i")) + return newStringFormulaArg(cmplx2str(result, "i")) } // IMTAN function returns the tangent of a supplied complex number. The syntax @@ -2781,11 +2818,12 @@ func (fn *formulaFuncs) IMTAN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMTAN requires 1 argument") } - inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + value := argsList.Front().Value.(formulaArg).Value() + inumber, err := strconv.ParseComplex(str2cmplx(value), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Tan(inumber)), "i")) + return newStringFormulaArg(cmplx2str(cmplx.Tan(inumber), value[len(value)-1:])) } // OCT2BIN function converts an Octal (Base 8) number into a Binary (Base 2) @@ -5652,7 +5690,8 @@ func logBeta(a, b float64) float64 { } if p >= 10.0 { corr = lgammacor(p) + lgammacor(q) - lgammacor(p+q) - return math.Log(q)*-0.5 + 0.918938533204672741780329736406 + corr + (p-0.5)*math.Log(p/(p+q)) + q*logrelerr(-p/(p+q)) + f1 := q * logrelerr(-p/(p+q)) + return math.Log(q)*-0.5 + 0.918938533204672741780329736406 + corr + (p-0.5)*math.Log(p/(p+q)) + math.Nextafter(f1, f1) } if q >= 10 { corr = lgammacor(q) - lgammacor(p+q) @@ -5970,6 +6009,108 @@ func (fn *formulaFuncs) BINOMDIST(argsList *list.List) formulaArg { return newNumberFormulaArg(binomdist(s.Number, trials.Number, probability.Number)) } +// BINOMdotDISTdotRANGE function returns the Binomial Distribution probability +// for the number of successes from a specified number of trials falling into +// a specified range. +// +// BINOM.DIST.RANGE(trials,probability_s,number_s,[number_s2]) +// +func (fn *formulaFuncs) BINOMdotDISTdotRANGE(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "BINOM.DIST.RANGE requires at least 3 arguments") + } + if argsList.Len() > 4 { + return newErrorFormulaArg(formulaErrorVALUE, "BINOM.DIST.RANGE requires at most 4 arguments") + } + trials := argsList.Front().Value.(formulaArg).ToNumber() + if trials.Type != ArgNumber { + return trials + } + probability := argsList.Front().Next().Value.(formulaArg).ToNumber() + if probability.Type != ArgNumber { + return probability + } + if probability.Number < 0 || probability.Number > 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + num1 := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if num1.Type != ArgNumber { + return num1 + } + if num1.Number < 0 || num1.Number > trials.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + num2 := num1 + if argsList.Len() > 3 { + if num2 = argsList.Back().Value.(formulaArg).ToNumber(); num2.Type != ArgNumber { + return num2 + } + } + if num2.Number < 0 || num2.Number > trials.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + sumn := 0.0 + for i := num1.Number; i <= num2.Number; i++ { + sumn += binomdist(i, trials.Number, probability.Number) + } + return newNumberFormulaArg(sumn) +} + +// binominv implement inverse of the binomial distribution calcuation. +func binominv(n, p, alpha float64) float64 { + q, i, sum, max := 1-p, 0.0, 0.0, 0.0 + n = math.Floor(n) + if q > p { + factor := math.Pow(q, n) + sum = factor + for i = 0; i < n && sum < alpha; i++ { + factor *= (n - i) / (i + 1) * p / q + sum += factor + } + return i + } + factor := math.Pow(p, n) + sum, max = 1-factor, n + for i = 0; i < max && sum >= alpha; i++ { + factor *= (n - i) / (i + 1) * q / p + sum -= factor + } + return n - i +} + +// BINOMdotINV function returns the inverse of the Cumulative Binomial +// Distribution. The syntax of the function is: +// +// BINOM.INV(trials,probability_s,alpha) +// +func (fn *formulaFuncs) BINOMdotINV(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "BINOM.INV requires 3 numeric arguments") + } + trials := argsList.Front().Value.(formulaArg).ToNumber() + if trials.Type != ArgNumber { + return trials + } + if trials.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + probability := argsList.Front().Next().Value.(formulaArg).ToNumber() + if probability.Type != ArgNumber { + return probability + } + if probability.Number <= 0 || probability.Number >= 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + alpha := argsList.Back().Value.(formulaArg).ToNumber() + if alpha.Type != ArgNumber { + return alpha + } + if alpha.Number <= 0 || alpha.Number >= 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(binominv(trials.Number, probability.Number, alpha.Number)) +} + // CHIDIST function calculates the right-tailed probability of the chi-square // distribution. The syntax of the function is: // @@ -7143,8 +7284,12 @@ func norminv(p float64) (float64, error) { // Rational approximation for central region. q := p - 0.5 r := q * q - return (((((a[1]*r+a[2])*r+a[3])*r+a[4])*r+a[5])*r + a[6]) * q / - (((((b[1]*r+b[2])*r+b[3])*r+b[4])*r+b[5])*r + 1), nil + f1 := ((((a[1]*r+a[2])*r+a[3])*r+a[4])*r + a[5]) * r + f2 := (b[1]*r + b[2]) * r + f3 := ((math.Nextafter(f2, f2)+b[3])*r + b[4]) * r + f4 := (math.Nextafter(f3, f3) + b[5]) * r + return (math.Nextafter(f1, f1) + a[6]) * q / + (math.Nextafter(f4, f4) + 1), nil } else if pHigh < p && p < 1 { // Rational approximation for upper region. q := math.Sqrt(-2 * math.Log(1-p)) @@ -7506,7 +7651,7 @@ func (fn *formulaFuncs) PERCENTILEdotEXC(argsList *list.List) formulaArg { idx := k.Number * (float64(cnt) + 1) base := math.Floor(idx) next := base - 1 - proportion := idx - base + proportion := math.Nextafter(idx, idx) - base return newNumberFormulaArg(numbers[int(next)] + ((numbers[int(base)] - numbers[int(next)]) * proportion)) } @@ -7559,7 +7704,7 @@ func (fn *formulaFuncs) PERCENTILE(argsList *list.List) formulaArg { return newNumberFormulaArg(numbers[int(idx)]) } next := base + 1 - proportion := idx - base + proportion := math.Nextafter(idx, idx) - base return newNumberFormulaArg(numbers[int(base)] + ((numbers[int(next)] - numbers[int(base)]) * proportion)) } @@ -14052,7 +14197,8 @@ func (fn *formulaFuncs) yield(settlement, maturity, rate, pr, redemption, freque yield2 = yieldN price2 = priceN } - yieldN.Number = yield2.Number - (yield2.Number-yield1.Number)*((pr.Number-price2.Number)/(price1.Number-price2.Number)) + f1 := (yield2.Number - yield1.Number) * ((pr.Number - price2.Number) / (price1.Number - price2.Number)) + yieldN.Number = yield2.Number - math.Nextafter(f1, f1) } } return yieldN @@ -14202,7 +14348,8 @@ func (fn *formulaFuncs) YIELDMAT(argsList *list.List) formulaArg { } dis := yearFrac(issue.Number, settlement.Number, int(basis.Number)) dsm := yearFrac(settlement.Number, maturity.Number, int(basis.Number)) - result := 1 + dim.Number*rate.Number + f1 := dim.Number * rate.Number + result := 1 + math.Nextafter(f1, f1) result /= pr.Number/100 + dis.Number*rate.Number result-- result /= dsm.Number diff --git a/calc_test.go b/calc_test.go index e0cd0d0e9a..cb09d26816 100644 --- a/calc_test.go +++ b/calc_test.go @@ -206,21 +206,21 @@ func TestCalcCellValue(t *testing.T) { // IMCOS "=IMCOS(0)": "1", "=IMCOS(0.5)": "0.877582561890373", - "=IMCOS(\"3+0.5i\")": "-1.1163412445261518-0.0735369737112366i", + "=IMCOS(\"3+0.5i\")": "-1.11634124452615-0.0735369737112366i", // IMCOSH "=IMCOSH(0.5)": "1.12762596520638", - "=IMCOSH(\"3+0.5i\")": "8.835204606500994+4.802825082743033i", - "=IMCOSH(\"2-i\")": "2.0327230070196656-3.0518977991518i", - "=IMCOSH(COMPLEX(1,-1))": "0.8337300251311491-0.9888977057628651i", + "=IMCOSH(\"3+0.5i\")": "8.83520460650099+4.80282508274303i", + "=IMCOSH(\"2-i\")": "2.03272300701967-3.0518977991518i", + "=IMCOSH(COMPLEX(1,-1))": "0.833730025131149-0.988897705762865i", // IMCOT "=IMCOT(0.5)": "1.83048772171245", - "=IMCOT(\"3+0.5i\")": "-0.4793455787473728-2.016092521506228i", - "=IMCOT(\"2-i\")": "-0.171383612909185+0.8213297974938518i", - "=IMCOT(COMPLEX(1,-1))": "0.21762156185440268+0.868014142895925i", + "=IMCOT(\"3+0.5i\")": "-0.479345578747373-2.01609252150623i", + "=IMCOT(\"2-i\")": "-0.171383612909185+0.821329797493852i", + "=IMCOT(COMPLEX(1,-1))": "0.217621561854403+0.868014142895925i", // IMCSC - "=IMCSC(\"j\")": "-0.8509181282393216i", + "=IMCSC(\"j\")": "-0.850918128239322j", // IMCSCH - "=IMCSCH(COMPLEX(1,-1))": "0.30393100162842646+0.6215180171704284i", + "=IMCSCH(COMPLEX(1,-1))": "0.303931001628426+0.621518017170428i", // IMDIV "=IMDIV(\"5+2i\",\"1+i\")": "3.5-1.5i", "=IMDIV(\"2+2i\",\"2+i\")": "1.2+0.4i", @@ -228,18 +228,18 @@ func TestCalcCellValue(t *testing.T) { // IMEXP "=IMEXP(0)": "1", "=IMEXP(0.5)": "1.64872127070013", - "=IMEXP(\"1-2i\")": "-1.1312043837568135-2.4717266720048183i", - "=IMEXP(COMPLEX(1,-1))": "1.4686939399158851-2.2873552871788423i", + "=IMEXP(\"1-2i\")": "-1.13120438375681-2.47172667200482i", + "=IMEXP(COMPLEX(1,-1))": "1.46869393991589-2.28735528717884i", // IMLN "=IMLN(0.5)": "-0.693147180559945", - "=IMLN(\"3+0.5i\")": "1.1123117757621668+0.16514867741462683i", - "=IMLN(\"2-i\")": "0.8047189562170503-0.4636476090008061i", - "=IMLN(COMPLEX(1,-1))": "0.3465735902799727-0.7853981633974483i", + "=IMLN(\"3+0.5i\")": "1.11231177576217+0.165148677414627i", + "=IMLN(\"2-i\")": "0.80471895621705-0.463647609000806i", + "=IMLN(COMPLEX(1,-1))": "0.346573590279973-0.785398163397448i", // IMLOG10 "=IMLOG10(0.5)": "-0.301029995663981", - "=IMLOG10(\"3+0.5i\")": "0.48307086636951624+0.07172315929479262i", - "=IMLOG10(\"2-i\")": "0.34948500216800943-0.20135959813668655i", - "=IMLOG10(COMPLEX(1,-1))": "0.1505149978319906-0.3410940884604603i", + "=IMLOG10(\"3+0.5i\")": "0.483070866369516+0.0717231592947926i", + "=IMLOG10(\"2-i\")": "0.349485002168009-0.201359598136687i", + "=IMLOG10(COMPLEX(1,-1))": "0.150514997831991-0.34109408846046i", // IMREAL "=IMREAL(\"5+2i\")": "5", "=IMREAL(\"2+2i\")": "2", @@ -248,31 +248,31 @@ func TestCalcCellValue(t *testing.T) { "=IMREAL(COMPLEX(4,1))": "4", // IMSEC "=IMSEC(0.5)": "1.13949392732455", - "=IMSEC(\"3+0.5i\")": "-0.8919131797403304+0.05875317818173977i", - "=IMSEC(\"2-i\")": "-0.4131493442669401-0.687527438655479i", - "=IMSEC(COMPLEX(1,-1))": "0.49833703055518686-0.5910838417210451i", + "=IMSEC(\"3+0.5i\")": "-0.89191317974033+0.0587531781817398i", + "=IMSEC(\"2-i\")": "-0.41314934426694-0.687527438655479i", + "=IMSEC(COMPLEX(1,-1))": "0.498337030555187-0.591083841721045i", // IMSECH "=IMSECH(0.5)": "0.886818883970074", - "=IMSECH(\"3+0.5i\")": "0.08736657796213027-0.047492549490160664i", - "=IMSECH(\"2-i\")": "0.1511762982655772+0.22697367539372157i", - "=IMSECH(COMPLEX(1,-1))": "0.49833703055518686+0.5910838417210451i", + "=IMSECH(\"3+0.5i\")": "0.0873665779621303-0.0474925494901607i", + "=IMSECH(\"2-i\")": "0.151176298265577+0.226973675393722i", + "=IMSECH(COMPLEX(1,-1))": "0.498337030555187+0.591083841721045i", // IMSIN "=IMSIN(0.5)": "0.479425538604203", - "=IMSIN(\"3+0.5i\")": "0.15913058529843999-0.5158804424525267i", - "=IMSIN(\"2-i\")": "1.4031192506220405+0.4890562590412937i", - "=IMSIN(COMPLEX(1,-1))": "1.2984575814159773-0.6349639147847361i", + "=IMSIN(\"3+0.5i\")": "0.15913058529844-0.515880442452527i", + "=IMSIN(\"2-i\")": "1.40311925062204+0.489056259041294i", + "=IMSIN(COMPLEX(1,-1))": "1.29845758141598-0.634963914784736i", // IMSINH "=IMSINH(-0)": "0", "=IMSINH(0.5)": "0.521095305493747", - "=IMSINH(\"3+0.5i\")": "8.791512343493714+4.82669427481082i", - "=IMSINH(\"2-i\")": "1.9596010414216063-3.165778513216168i", - "=IMSINH(COMPLEX(1,-1))": "0.6349639147847361-1.2984575814159773i", + "=IMSINH(\"3+0.5i\")": "8.79151234349371+4.82669427481082i", + "=IMSINH(\"2-i\")": "1.95960104142161-3.16577851321617i", + "=IMSINH(COMPLEX(1,-1))": "0.634963914784736-1.29845758141598i", // IMSQRT - "=IMSQRT(\"i\")": "0.7071067811865476+0.7071067811865476i", - "=IMSQRT(\"2-i\")": "1.455346690225355-0.34356074972251244i", - "=IMSQRT(\"5+2i\")": "2.27872385417085+0.4388421169022545i", + "=IMSQRT(\"i\")": "0.707106781186548+0.707106781186548i", + "=IMSQRT(\"2-i\")": "1.45534669022535-0.343560749722512i", + "=IMSQRT(\"5+2i\")": "2.27872385417085+0.438842116902254i", "=IMSQRT(6)": "2.44948974278318", - "=IMSQRT(\"-2-4i\")": "1.1117859405028423-1.7989074399478673i", + "=IMSQRT(\"-2-4i\")": "1.11178594050284-1.79890743994787i", // IMSUB "=IMSUB(\"5+i\",\"1+4i\")": "4-3i", "=IMSUB(\"9+2i\",6)": "3+2i", @@ -283,9 +283,9 @@ func TestCalcCellValue(t *testing.T) { // IMTAN "=IMTAN(-0)": "0", "=IMTAN(0.5)": "0.54630248984379", - "=IMTAN(\"3+0.5i\")": "-0.11162105077158344+0.46946999342588536i", - "=IMTAN(\"2-i\")": "-0.24345820118572523-1.16673625724092i", - "=IMTAN(COMPLEX(1,-1))": "0.2717525853195117-1.0839233273386948i", + "=IMTAN(\"3+0.5i\")": "-0.111621050771583+0.469469993425885i", + "=IMTAN(\"2-i\")": "-0.243458201185725-1.16673625724092i", + "=IMTAN(COMPLEX(1,-1))": "0.271752585319512-1.08392332733869i", // OCT2BIN "=OCT2BIN(\"5\")": "101", "=OCT2BIN(\"0000000001\")": "1", @@ -555,16 +555,16 @@ func TestCalcCellValue(t *testing.T) { "=LOG10(25)": "1.39794000867204", "=LOG10(LOG10(100))": "0.301029995663981", // IMLOG2 - "=IMLOG2(\"5+2i\")": "2.4289904975637864+0.5489546632866347i", - "=IMLOG2(\"2-i\")": "1.1609640474436813-0.6689021062254881i", + "=IMLOG2(\"5+2i\")": "2.42899049756379+0.548954663286635i", + "=IMLOG2(\"2-i\")": "1.16096404744368-0.668902106225488i", "=IMLOG2(6)": "2.58496250072116", - "=IMLOG2(\"3i\")": "1.584962500721156+2.266180070913597i", - "=IMLOG2(\"4+i\")": "2.04373142062517+0.3534295024167349i", + "=IMLOG2(\"3i\")": "1.58496250072116+2.2661800709136i", + "=IMLOG2(\"4+i\")": "2.04373142062517+0.353429502416735i", // IMPOWER - "=IMPOWER(\"2-i\",2)": "3.000000000000001-4i", - "=IMPOWER(\"2-i\",3)": "2.0000000000000018-11.000000000000002i", + "=IMPOWER(\"2-i\",2)": "3-4i", + "=IMPOWER(\"2-i\",3)": "2-11i", "=IMPOWER(9,0.5)": "3", - "=IMPOWER(\"2+4i\",-2)": "-0.029999999999999985-0.039999999999999994i", + "=IMPOWER(\"2+4i\",-2)": "-0.03-0.04i", // IMPRODUCT "=IMPRODUCT(3,6)": "18", `=IMPRODUCT("",3,SUM(6))`: "18", @@ -819,6 +819,19 @@ func TestCalcCellValue(t *testing.T) { "=BINOM.DIST(10,100,0.5,TRUE)": "1.53164508771899E-17", "=BINOM.DIST(50,100,0.5,TRUE)": "0.539794618693589", "=BINOM.DIST(65,100,0.5,TRUE)": "0.999105034804256", + // BINOM.DIST.RANGE + "=BINOM.DIST.RANGE(100,0.5,0,40)": "0.0284439668204904", + "=BINOM.DIST.RANGE(100,0.5,45,55)": "0.728746975926165", + "=BINOM.DIST.RANGE(100,0.5,50,100)": "0.539794618693589", + "=BINOM.DIST.RANGE(100,0.5,50)": "0.0795892373871787", + // BINOM.INV + "=BINOM.INV(0,0.5,0.75)": "0", + "=BINOM.INV(0.1,0.1,0.75)": "0", + "=BINOM.INV(0.6,0.4,0.75)": "0", + "=BINOM.INV(2,0.4,0.75)": "1", + "=BINOM.INV(100,0.5,20%)": "46", + "=BINOM.INV(100,0.5,50%)": "50", + "=BINOM.INV(100,0.5,90%)": "56", // CHIDIST "=CHIDIST(0.5,3)": "0.918891411654676", "=CHIDIST(8,3)": "0.0460117056892315", @@ -2436,6 +2449,30 @@ func TestCalcCellValue(t *testing.T) { "=BINOM.DIST(110,100,0.5,FALSE)": "#NUM!", "=BINOM.DIST(10,100,-1,FALSE)": "#NUM!", "=BINOM.DIST(10,100,2,FALSE)": "#NUM!", + // BINOM.DIST.RANGE + "=BINOM.DIST.RANGE()": "BINOM.DIST.RANGE requires at least 3 arguments", + "=BINOM.DIST.RANGE(100,0.5,0,40,0)": "BINOM.DIST.RANGE requires at most 4 arguments", + "=BINOM.DIST.RANGE(\"\",0.5,0,40)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOM.DIST.RANGE(100,\"\",0,40)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOM.DIST.RANGE(100,0.5,\"\",40)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOM.DIST.RANGE(100,0.5,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOM.DIST.RANGE(100,-1,0,40)": "#NUM!", + "=BINOM.DIST.RANGE(100,2,0,40)": "#NUM!", + "=BINOM.DIST.RANGE(100,0.5,-1,40)": "#NUM!", + "=BINOM.DIST.RANGE(100,0.5,110,40)": "#NUM!", + "=BINOM.DIST.RANGE(100,0.5,0,-1)": "#NUM!", + "=BINOM.DIST.RANGE(100,0.5,0,110)": "#NUM!", + // BINOM.INV + "=BINOM.INV()": "BINOM.INV requires 3 numeric arguments", + "=BINOM.INV(\"\",0.5,20%)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOM.INV(100,\"\",20%)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOM.INV(100,0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BINOM.INV(-1,0.5,20%)": "#NUM!", + "=BINOM.INV(100,-1,20%)": "#NUM!", + "=BINOM.INV(100,2,20%)": "#NUM!", + "=BINOM.INV(100,0.5,-1)": "#NUM!", + "=BINOM.INV(100,0.5,2)": "#NUM!", + "=BINOM.INV(1,1,20%)": "#NUM!", // CHIDIST "=CHIDIST()": "CHIDIST requires 2 numeric arguments", "=CHIDIST(\"\",3)": "strconv.ParseFloat: parsing \"\": invalid syntax", From 797958210d018a7b898737309b53de53d050316a Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 22 Mar 2022 00:03:29 +0800 Subject: [PATCH 009/213] ref #65, new formula functions: CRITBINOM and SUMIFS --- calc.go | 44 ++++++++++++++++++++++++++++++++++++++- calc_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/calc.go b/calc.go index 38be45cf7f..fbaf961ae3 100644 --- a/calc.go +++ b/calc.go @@ -385,6 +385,7 @@ type formulaFuncs struct { // COUPPCD // COVAR // COVARIANCE.P +// CRITBINOM // CSC // CSCH // CUMIPMT @@ -624,6 +625,7 @@ type formulaFuncs struct { // SUBSTITUTE // SUM // SUMIF +// SUMIFS // SUMSQ // SUMX2MY2 // SUMX2PY2 @@ -4968,6 +4970,31 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { return newNumberFormulaArg(sum) } +// SUMIFS function finds values in one or more supplied arrays, that satisfy a +// set of criteria, and returns the sum of the corresponding values in a +// further supplied array. The syntax of the function is: +// +// SUMIFS(sum_range,criteria_range1,criteria1,[criteria_range2,criteria2],...) +// +func (fn *formulaFuncs) SUMIFS(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "SUMIFS requires at least 3 arguments") + } + if argsList.Len()%2 != 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + sum, sumRange, args := 0.0, argsList.Front().Value.(formulaArg).Matrix, []formulaArg{} + for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() { + args = append(args, arg.Value.(formulaArg)) + } + for _, ref := range formulaIfsMatch(args) { + if num := sumRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber { + sum += num.Number + } + } + return newNumberFormulaArg(sum) +} + // SUMSQ function returns the sum of squares of a supplied set of values. The // syntax of the function is: // @@ -5956,7 +5983,7 @@ func binomdist(x, n, p float64) float64 { return binomCoeff(n, x) * math.Pow(p, x) * math.Pow(1-p, n-x) } -// BINOMfotDIST function returns the Binomial Distribution probability for a +// BINOMdotDIST function returns the Binomial Distribution probability for a // given number of successes from a specified number of trials. The syntax of // the function is: // @@ -6492,6 +6519,21 @@ func (fn *formulaFuncs) COUNTIFS(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(len(formulaIfsMatch(args)))) } +// CRITBINOM function returns the inverse of the Cumulative Binomial +// Distribution. I.e. for a specific number of independent trials, the +// function returns the smallest value (number of successes) for which the +// cumulative binomial distribution is greater than or equal to a specified +// value. The syntax of the function is: +// +// CRITBINOM(trials,probability_s,alpha) +// +func (fn *formulaFuncs) CRITBINOM(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "CRITBINOM requires 3 numeric arguments") + } + return fn.BINOMdotINV(argsList) +} + // DEVSQ function calculates the sum of the squared deviations from the sample // mean. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index cb09d26816..23af1730c2 100644 --- a/calc_test.go +++ b/calc_test.go @@ -867,6 +867,14 @@ func TestCalcCellValue(t *testing.T) { "=COUNTIFS(A1:A9,2,D1:D9,\"Jan\")": "1", "=COUNTIFS(F1:F9,\">20000\",D1:D9,\"Jan\")": "4", "=COUNTIFS(F1:F9,\">60000\",D1:D9,\"Jan\")": "0", + // CRITBINOM + "=CRITBINOM(0,0.5,0.75)": "0", + "=CRITBINOM(0.1,0.1,0.75)": "0", + "=CRITBINOM(0.6,0.4,0.75)": "0", + "=CRITBINOM(2,0.4,0.75)": "1", + "=CRITBINOM(100,0.5,20%)": "46", + "=CRITBINOM(100,0.5,50%)": "50", + "=CRITBINOM(100,0.5,90%)": "56", // DEVSQ "=DEVSQ(1,3,5,2,9,7)": "47.5", "=DEVSQ(A1:D2)": "10", @@ -2514,6 +2522,17 @@ func TestCalcCellValue(t *testing.T) { // COUNTIFS "=COUNTIFS()": "COUNTIFS requires at least 2 arguments", "=COUNTIFS(A1:A9,2,D1:D9)": "#N/A", + // CRITBINOM + "=CRITBINOM()": "CRITBINOM requires 3 numeric arguments", + "=CRITBINOM(\"\",0.5,20%)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CRITBINOM(100,\"\",20%)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CRITBINOM(100,0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CRITBINOM(-1,0.5,20%)": "#NUM!", + "=CRITBINOM(100,-1,20%)": "#NUM!", + "=CRITBINOM(100,2,20%)": "#NUM!", + "=CRITBINOM(100,0.5,-1)": "#NUM!", + "=CRITBINOM(100,0.5,2)": "#NUM!", + "=CRITBINOM(1,1,20%)": "#NUM!", // DEVSQ "=DEVSQ()": "DEVSQ requires at least 1 numeric argument", "=DEVSQ(D1:D2)": "#N/A", @@ -4212,6 +4231,45 @@ func TestCalcMIRR(t *testing.T) { } } +func TestCalcSUMIFS(t *testing.T) { + cellData := [][]interface{}{ + {"Quarter", "Area", "Sales Rep.", "Sales"}, + {1, "North", "Jeff", 223000}, + {1, "North", "Chris", 125000}, + {1, "South", "Carol", 456000}, + {2, "North", "Jeff", 322000}, + {2, "North", "Chris", 340000}, + {2, "South", "Carol", 198000}, + {3, "North", "Jeff", 310000}, + {3, "North", "Chris", 250000}, + {3, "South", "Carol", 460000}, + {4, "North", "Jeff", 261000}, + {4, "North", "Chris", 389000}, + {4, "South", "Carol", 305000}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=SUMIFS(D2:D13,A2:A13,1,B2:B13,\"North\")": "348000", + "=SUMIFS(D2:D13,A2:A13,\">2\",C2:C13,\"Jeff\")": "571000", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula)) + result, err := f.CalcCellValue("Sheet1", "E1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError := map[string]string{ + "=SUMIFS()": "SUMIFS requires at least 3 arguments", + "=SUMIFS(D2:D13,A2:A13,1,B2:B13)": "#N/A", + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula)) + result, err := f.CalcCellValue("Sheet1", "E1") + assert.EqualError(t, err, expected, formula) + assert.Equal(t, "", result, formula) + } +} + func TestCalcXIRR(t *testing.T) { cellData := [][]interface{}{ {-100.00, "01/01/2016"}, From 139ee4c4b0c86dffbdca77da346e85a4cbd97b0c Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 23 Mar 2022 08:14:19 +0800 Subject: [PATCH 010/213] ref #65, new formula functions: AVERAGEIFS and SUMPRODUCT --- calc.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 25 +++++++++++-- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/calc.go b/calc.go index fbaf961ae3..e2dec3286d 100644 --- a/calc.go +++ b/calc.go @@ -328,6 +328,7 @@ type formulaFuncs struct { // AVERAGE // AVERAGEA // AVERAGEIF +// AVERAGEIFS // BASE // BESSELI // BESSELJ @@ -626,6 +627,7 @@ type formulaFuncs struct { // SUM // SUMIF // SUMIFS +// SUMPRODUCT // SUMSQ // SUMX2MY2 // SUMX2PY2 @@ -4995,6 +4997,73 @@ func (fn *formulaFuncs) SUMIFS(argsList *list.List) formulaArg { return newNumberFormulaArg(sum) } +// sumproduct is an implementation of the formula function SUMPRODUCT. +func (fn *formulaFuncs) sumproduct(argsList *list.List) formulaArg { + var ( + argType ArgType + n int + res []float64 + sum float64 + ) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + if argType == ArgUnknown { + argType = token.Type + } + if token.Type != argType { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + switch token.Type { + case ArgString, ArgNumber: + if num := token.ToNumber(); num.Type == ArgNumber { + sum = fn.PRODUCT(argsList).Number + continue + } + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + case ArgMatrix: + args := token.ToList() + if res == nil { + n = len(args) + res = make([]float64, n) + for i := range res { + res[i] = 1.0 + } + } + if len(args) != n { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + for i, value := range args { + num := value.ToNumber() + if num.Type != ArgNumber && value.Value() != "" { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + res[i] = res[i] * num.Number + } + } + } + for _, r := range res { + sum += r + } + return newNumberFormulaArg(sum) +} + +// SUMPRODUCT function returns the sum of the products of the corresponding +// values in a set of supplied arrays. The syntax of the function is: +// +// SUMPRODUCT(array1,[array2],[array3],...) +// +func (fn *formulaFuncs) SUMPRODUCT(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "SUMPRODUCT requires at least 1 argument") + } + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + if token := arg.Value.(formulaArg); token.Type == ArgError { + return token + } + } + return fn.sumproduct(argsList) +} + // SUMSQ function returns the sum of squares of a supplied set of values. The // syntax of the function is: // @@ -5270,6 +5339,37 @@ func (fn *formulaFuncs) AVERAGEIF(argsList *list.List) formulaArg { return newNumberFormulaArg(sum / count) } +// AVERAGEIFS function finds entries in one or more arrays, that satisfy a set +// of supplied criteria, and returns the average (i.e. the statistical mean) +// of the corresponding values in a further supplied array. The syntax of the +// function is: +// +// AVERAGEIFS(average_range,criteria_range1,criteria1,[criteria_range2,criteria2],...) +// +func (fn *formulaFuncs) AVERAGEIFS(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "AVERAGEIFS requires at least 3 arguments") + } + if argsList.Len()%2 != 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + sum, sumRange, args := 0.0, argsList.Front().Value.(formulaArg).Matrix, []formulaArg{} + for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() { + args = append(args, arg.Value.(formulaArg)) + } + count := 0.0 + for _, ref := range formulaIfsMatch(args) { + if num := sumRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber { + sum += num.Number + count++ + } + } + if count == 0 { + return newErrorFormulaArg(formulaErrorDIV, "AVERAGEIF divide by zero") + } + return newNumberFormulaArg(sum / count) +} + // getBetaHelperContFrac continued fractions for the beta function. func getBetaHelperContFrac(fX, fA, fB float64) float64 { var a1, b1, a2, b2, fnorm, cfnew, cf, rm float64 diff --git a/calc_test.go b/calc_test.go index 23af1730c2..4025ceca11 100644 --- a/calc_test.go +++ b/calc_test.go @@ -741,6 +741,12 @@ func TestCalcCellValue(t *testing.T) { `=SUMIF(E2:E9,"North 1",F2:F9)`: "66582", `=SUMIF(E2:E9,"North*",F2:F9)`: "138772", "=SUMIF(D1:D3,\"Month\",D1:D3)": "0", + // SUMPRODUCT + "=SUMPRODUCT(A1,B1)": "4", + "=SUMPRODUCT(A1:A2,B1:B2)": "14", + "=SUMPRODUCT(A1:A3,B1:B3)": "14", + "=SUMPRODUCT(A1:B3)": "15", + "=SUMPRODUCT(A1:A3,B1:B3,B2:B4)": "20", // SUMSQ "=SUMSQ(A1:A4)": "14", "=SUMSQ(A1,B1,A2,B2,6)": "82", @@ -2351,6 +2357,13 @@ func TestCalcCellValue(t *testing.T) { // SUMSQ `=SUMSQ("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=SUMSQ(C1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", + // SUMPRODUCT + "=SUMPRODUCT()": "SUMPRODUCT requires at least 1 argument", + "=SUMPRODUCT(A1,B1:B2)": "#VALUE!", + "=SUMPRODUCT(A1,D1)": "#VALUE!", + "=SUMPRODUCT(A1:A3,D1:D3)": "#VALUE!", + "=SUMPRODUCT(A1:A2,B1:B3)": "#VALUE!", + "=SUMPRODUCT(A1,NA())": "#N/A", // SUMX2MY2 "=SUMX2MY2()": "SUMX2MY2 requires 2 arguments", "=SUMX2MY2(A1,B1:B2)": "#N/A", @@ -4231,7 +4244,7 @@ func TestCalcMIRR(t *testing.T) { } } -func TestCalcSUMIFS(t *testing.T) { +func TestCalcSUMIFSAndAVERAGEIFS(t *testing.T) { cellData := [][]interface{}{ {"Quarter", "Area", "Sales Rep.", "Sales"}, {1, "North", "Jeff", 223000}, @@ -4249,8 +4262,10 @@ func TestCalcSUMIFS(t *testing.T) { } f := prepareCalcData(cellData) formulaList := map[string]string{ - "=SUMIFS(D2:D13,A2:A13,1,B2:B13,\"North\")": "348000", - "=SUMIFS(D2:D13,A2:A13,\">2\",C2:C13,\"Jeff\")": "571000", + "=AVERAGEIFS(D2:D13,A2:A13,1,B2:B13,\"North\")": "174000", + "=AVERAGEIFS(D2:D13,A2:A13,\">2\",C2:C13,\"Jeff\")": "285500", + "=SUMIFS(D2:D13,A2:A13,1,B2:B13,\"North\")": "348000", + "=SUMIFS(D2:D13,A2:A13,\">2\",C2:C13,\"Jeff\")": "571000", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula)) @@ -4259,6 +4274,10 @@ func TestCalcSUMIFS(t *testing.T) { assert.Equal(t, expected, result, formula) } calcError := map[string]string{ + "=AVERAGEIFS()": "AVERAGEIFS requires at least 3 arguments", + "=AVERAGEIFS(H1,\"\")": "AVERAGEIFS requires at least 3 arguments", + "=AVERAGEIFS(H1,\"\",TRUE,1)": "#N/A", + "=AVERAGEIFS(H1,\"\",TRUE)": "AVERAGEIF divide by zero", "=SUMIFS()": "SUMIFS requires at least 3 arguments", "=SUMIFS(D2:D13,A2:A13,1,B2:B13)": "#N/A", } From 8a335225c705232fe1174755a1b1ea475456b864 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 24 Mar 2022 00:19:30 +0800 Subject: [PATCH 011/213] Format code, update documentation and remove exported variable `XMLHeaderByte` --- calc.go | 366 +++++++++++++++++++++++---------------------- calc_test.go | 2 +- cell.go | 55 ++++--- cell_test.go | 2 +- chart.go | 2 +- chart_test.go | 2 +- col.go | 21 ++- comment.go | 2 +- crypt.go | 6 +- crypt_test.go | 2 +- datavalidation.go | 21 +-- drawing.go | 2 +- errors.go | 4 +- excelize.go | 9 +- excelize_test.go | 14 +- lib.go | 26 ++-- merge_test.go | 2 +- numfmt.go | 10 +- picture.go | 4 +- pivotTable.go | 10 +- pivotTable_test.go | 6 +- rows.go | 2 +- sheet.go | 42 +++--- sheet_test.go | 2 +- sheetview.go | 4 +- sparkline.go | 2 +- stream.go | 2 +- stream_test.go | 2 +- styles.go | 6 +- styles_test.go | 4 +- table.go | 8 +- templates.go | 6 - xmlCalcChain.go | 4 +- xmlContentTypes.go | 2 +- xmlStyles.go | 2 +- xmlTable.go | 2 +- 36 files changed, 330 insertions(+), 328 deletions(-) diff --git a/calc.go b/calc.go index e2dec3286d..f707ee5c3e 100644 --- a/calc.go +++ b/calc.go @@ -282,11 +282,11 @@ func (fa formulaArg) ToBool() formulaArg { func (fa formulaArg) ToList() []formulaArg { switch fa.Type { case ArgMatrix: - list := []formulaArg{} + var args []formulaArg for _, row := range fa.Matrix { - list = append(list, row...) + args = append(args, row...) } - return list + return args case ArgList: return fa.List case ArgNumber, ArgString, ArgError, ArgUnknown: @@ -1452,7 +1452,7 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (arg formulaArg, e if cellRanges.Len() > 0 { arg.Type = ArgMatrix for row := valueRange[0]; row <= valueRange[1]; row++ { - matrixRow := []formulaArg{} + var matrixRow []formulaArg for col := valueRange[2]; col <= valueRange[3]; col++ { var cell, value string if cell, err = CoordinatesToCellName(col, row); err != nil { @@ -1629,10 +1629,11 @@ func (fn *formulaFuncs) bassel(argsList *list.List, modfied bool) formulaArg { n4++ n2 *= n4 t = result + r := x1 / n1 / n2 if modfied || add { - result += (x1 / n1 / n2) + result += r } else { - result -= (x1 / n1 / n2) + result -= r } max-- add = !add @@ -1979,9 +1980,9 @@ func (fn *formulaFuncs) COMPLEX(argsList *list.List) formulaArg { if argsList.Len() > 3 { return newErrorFormulaArg(formulaErrorVALUE, "COMPLEX allows at most 3 arguments") } - real, i, suffix := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Front().Next().Value.(formulaArg).ToNumber(), "i" - if real.Type != ArgNumber { - return real + realNum, i, suffix := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Front().Next().Value.(formulaArg).ToNumber(), "i" + if realNum.Type != ArgNumber { + return realNum } if i.Type != ArgNumber { return i @@ -1991,7 +1992,7 @@ func (fn *formulaFuncs) COMPLEX(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } } - return newStringFormulaArg(cmplx2str(complex(real.Number, i.Number), suffix)) + return newStringFormulaArg(cmplx2str(complex(realNum.Number, i.Number), suffix)) } // cmplx2str replace complex number string characters. @@ -2226,7 +2227,7 @@ func (fn *formulaFuncs) ERFC(argsList *list.List) formulaArg { return fn.erfc("ERFC", argsList) } -// ERFC.PRECISE function calculates the Complementary Error Function, +// ERFCdotPRECISE function calculates the Complementary Error Function, // integrated between a supplied lower limit and infinity. The syntax of the // function is: // @@ -3773,7 +3774,7 @@ func (fn *formulaFuncs) GCD(argsList *list.List) formulaArg { } var ( val float64 - nums = []float64{} + nums []float64 ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) @@ -3890,7 +3891,7 @@ func (fn *formulaFuncs) LCM(argsList *list.List) formulaArg { } var ( val float64 - nums = []float64{} + nums []float64 err error ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { @@ -3995,12 +3996,12 @@ func (fn *formulaFuncs) LOG10(argsList *list.List) formulaArg { // minor function implement a minor of a matrix A is the determinant of some // smaller square matrix. func minor(sqMtx [][]float64, idx int) [][]float64 { - ret := [][]float64{} + var ret [][]float64 for i := range sqMtx { if i == 0 { continue } - row := []float64{} + var row []float64 for j := range sqMtx { if j == idx { continue @@ -4037,7 +4038,7 @@ func det(sqMtx [][]float64) float64 { func (fn *formulaFuncs) MDETERM(argsList *list.List) (result formulaArg) { var ( num float64 - numMtx = [][]float64{} + numMtx [][]float64 err error strMtx [][]formulaArg ) @@ -4050,7 +4051,7 @@ func (fn *formulaFuncs) MDETERM(argsList *list.List) (result formulaArg) { if len(row) != rows { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - numRow := []float64{} + var numRow []float64 for _, ele := range row { if num, err = strconv.ParseFloat(ele.String, 64); err != nil { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) @@ -4783,9 +4784,9 @@ func (fn *formulaFuncs) STDEVA(argsList *list.List) formulaArg { // calcStdevPow is part of the implementation stdev. func calcStdevPow(result, count float64, n, m formulaArg) (float64, float64) { if result == -1 { - result = math.Pow((n.Number - m.Number), 2) + result = math.Pow(n.Number-m.Number, 2) } else { - result += math.Pow((n.Number - m.Number), 2) + result += math.Pow(n.Number-m.Number, 2) } count++ return result, count @@ -4985,7 +4986,8 @@ func (fn *formulaFuncs) SUMIFS(argsList *list.List) formulaArg { if argsList.Len()%2 != 1 { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - sum, sumRange, args := 0.0, argsList.Front().Value.(formulaArg).Matrix, []formulaArg{} + var args []formulaArg + sum, sumRange := 0.0, argsList.Front().Value.(formulaArg).Matrix for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() { args = append(args, arg.Value.(formulaArg)) } @@ -5260,7 +5262,7 @@ func (fn *formulaFuncs) AVEDEV(argsList *list.List) formulaArg { // AVERAGE(number1,[number2],...) // func (fn *formulaFuncs) AVERAGE(argsList *list.List) formulaArg { - args := []formulaArg{} + var args []formulaArg for arg := argsList.Front(); arg != nil; arg = arg.Next() { args = append(args, arg.Value.(formulaArg)) } @@ -5277,7 +5279,7 @@ func (fn *formulaFuncs) AVERAGE(argsList *list.List) formulaArg { // AVERAGEA(number1,[number2],...) // func (fn *formulaFuncs) AVERAGEA(argsList *list.List) formulaArg { - args := []formulaArg{} + var args []formulaArg for arg := argsList.Front(); arg != nil; arg = arg.Next() { args = append(args, arg.Value.(formulaArg)) } @@ -5353,7 +5355,8 @@ func (fn *formulaFuncs) AVERAGEIFS(argsList *list.List) formulaArg { if argsList.Len()%2 != 1 { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - sum, sumRange, args := 0.0, argsList.Front().Value.(formulaArg).Matrix, []formulaArg{} + var args []formulaArg + sum, sumRange := 0.0, argsList.Front().Value.(formulaArg).Matrix for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() { args = append(args, arg.Value.(formulaArg)) } @@ -5394,7 +5397,7 @@ func getBetaHelperContFrac(fX, fA, fB float64) float64 { if b2 != 0 { fnorm = 1 / b2 cfnew = a2 * fnorm - bfinished = (math.Abs(cf-cfnew) < math.Abs(cf)*fMachEps) + bfinished = math.Abs(cf-cfnew) < math.Abs(cf)*fMachEps } cf = cfnew rm += 1 @@ -5911,12 +5914,12 @@ func pbeta(x, pin, qin float64) (ans float64) { // betainvProbIterator is a part of betainv for the inverse of the beta // function. -func betainvProbIterator(alpha1, alpha3, beta1, beta2, beta3, logbeta, lower, maxCumulative, prob1, prob2, upper float64, needSwap bool) float64 { +func betainvProbIterator(alpha1, alpha3, beta1, beta2, beta3, logBeta, maxCumulative, prob1, prob2 float64) float64 { var i, j, prev, prop4 float64 j = 1 for prob := 0; prob < 1000; prob++ { prop3 := pbeta(beta3, alpha1, beta1) - prop3 = (prop3 - prob1) * math.Exp(logbeta+prob2*math.Log(beta3)+beta2*math.Log(1.0-beta3)) + prop3 = (prop3 - prob1) * math.Exp(logBeta+prob2*math.Log(beta3)+beta2*math.Log(1.0-beta3)) if prop3*prop4 <= 0 { prev = math.Max(math.Abs(j), maxCumulative) } @@ -5959,7 +5962,7 @@ func calcBetainv(probability, alpha, beta, lower, upper float64) float64 { } else { prob1, alpha1, beta1, needSwap = 1.0-probability, beta, alpha, true } - logbeta := logBeta(alpha, beta) + logBetaNum := logBeta(alpha, beta) prob2 := math.Sqrt(-math.Log(prob1 * prob1)) prob3 := prob2 - (prob2*0.27061+2.3075)/(prob2*(prob2*0.04481+0.99229)+1) if alpha1 > 1 && beta1 > 1 { @@ -5971,11 +5974,11 @@ func calcBetainv(probability, alpha, beta, lower, upper float64) float64 { beta2, prob2 = 1/(beta1*9), beta1+beta1 beta2 = prob2 * math.Pow(1-beta2+prob3*math.Sqrt(beta2), 3) if beta2 <= 0 { - beta3 = 1 - math.Exp((math.Log((1-prob1)*beta1)+logbeta)/beta1) + beta3 = 1 - math.Exp((math.Log((1-prob1)*beta1)+logBetaNum)/beta1) } else { beta2 = (prob2 + alpha1*4 - 2) / beta2 if beta2 <= 1 { - beta3 = math.Exp((logbeta + math.Log(alpha1*prob1)) / alpha1) + beta3 = math.Exp((logBetaNum + math.Log(alpha1*prob1)) / alpha1) } else { beta3 = 1 - 2/(beta2+1) } @@ -5988,7 +5991,7 @@ func calcBetainv(probability, alpha, beta, lower, upper float64) float64 { beta3 = upperBound } alpha3 := math.Max(minCumulative, math.Pow(10.0, -13.0-2.5/(alpha1*alpha1)-0.5/(prob1*prob1))) - beta3 = betainvProbIterator(alpha1, alpha3, beta1, beta2, beta3, logbeta, lower, maxCumulative, prob1, prob2, upper, needSwap) + beta3 = betainvProbIterator(alpha1, alpha3, beta1, beta2, beta3, logBetaNum, maxCumulative, prob1, prob2) if needSwap { beta3 = 1.0 - beta3 } @@ -6066,19 +6069,19 @@ func incompleteGamma(a, x float64) float64 { for n := 0; n <= max; n++ { divisor := a for i := 1; i <= n; i++ { - divisor *= (a + float64(i)) + divisor *= a + float64(i) } summer += math.Pow(x, float64(n)) / divisor } return math.Pow(x, a) * math.Exp(0-x) * summer } -// binomCoeff implement binomial coefficient calcuation. +// binomCoeff implement binomial coefficient calculation. func binomCoeff(n, k float64) float64 { return fact(n) / (fact(k) * fact(n-k)) } -// binomdist implement binomial distribution calcuation. +// binomdist implement binomial distribution calculation. func binomdist(x, n, p float64) float64 { return binomCoeff(n, x) * math.Pow(p, x) * math.Pow(1-p, n-x) } @@ -6176,11 +6179,11 @@ func (fn *formulaFuncs) BINOMdotDISTdotRANGE(argsList *list.List) formulaArg { if num2.Number < 0 || num2.Number > trials.Number { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - sumn := 0.0 + sum := 0.0 for i := num1.Number; i <= num2.Number; i++ { - sumn += binomdist(i, trials.Number, probability.Number) + sum += binomdist(i, trials.Number, probability.Number) } - return newNumberFormulaArg(sumn) + return newNumberFormulaArg(sum) } // binominv implement inverse of the binomial distribution calcuation. @@ -6261,7 +6264,7 @@ func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { // CHIINV function calculates the inverse of the right-tailed probability of // the Chi-Square Distribution. The syntax of the function is: // -// CHIINV(probability,degrees_freedom) +// CHIINV(probability,deg_freedom) // func (fn *formulaFuncs) CHIINV(argsList *list.List) formulaArg { if argsList.Len() != 2 { @@ -6274,14 +6277,14 @@ func (fn *formulaFuncs) CHIINV(argsList *list.List) formulaArg { if probability.Number <= 0 || probability.Number > 1 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - degress := argsList.Back().Value.(formulaArg).ToNumber() - if degress.Type != ArgNumber { - return degress + deg := argsList.Back().Value.(formulaArg).ToNumber() + if deg.Type != ArgNumber { + return deg } - if degress.Number < 1 { + if deg.Number < 1 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newNumberFormulaArg(gammainv(1-probability.Number, 0.5*degress.Number, 2.0)) + return newNumberFormulaArg(gammainv(1-probability.Number, 0.5*deg.Number, 2.0)) } // confidence is an implementation of the formula functions CONFIDENCE and @@ -6321,7 +6324,7 @@ func (fn *formulaFuncs) confidence(name string, argsList *list.List) formulaArg // CONFIDENCE function uses a Normal Distribution to calculate a confidence // value that can be used to construct the Confidence Interval for a -// population mean, for a supplied probablity and sample size. It is assumed +// population mean, for a supplied probability and sample size. It is assumed // that the standard deviation of the population is known. The syntax of the // function is: // @@ -6333,7 +6336,7 @@ func (fn *formulaFuncs) CONFIDENCE(argsList *list.List) formulaArg { // CONFIDENCEdotNORM function uses a Normal Distribution to calculate a // confidence value that can be used to construct the confidence interval for -// a population mean, for a supplied probablity and sample size. It is +// a population mean, for a supplied probability and sample size. It is // assumed that the standard deviation of the population is known. The syntax // of the Confidence.Norm function is: // @@ -6574,7 +6577,7 @@ func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg { // formulaIfsMatch function returns cells reference array which match criteria. func formulaIfsMatch(args []formulaArg) (cellRefs []cellRef) { for i := 0; i < len(args)-1; i += 2 { - match := []cellRef{} + var match []cellRef matrix, criteria := args[i].Matrix, formulaCriteriaParser(args[i+1].Value()) if i == 0 { for rowIdx, row := range matrix { @@ -6612,7 +6615,7 @@ func (fn *formulaFuncs) COUNTIFS(argsList *list.List) formulaArg { if argsList.Len()%2 != 0 { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - args := []formulaArg{} + var args []formulaArg for arg := argsList.Front(); arg != nil; arg = arg.Next() { args = append(args, arg.Value.(formulaArg)) } @@ -6787,7 +6790,7 @@ func (fn *formulaFuncs) GAMMADIST(argsList *list.List) formulaArg { if cumulative.Number == 1 { return newNumberFormulaArg(incompleteGamma(alpha.Number, x.Number/beta.Number) / math.Gamma(alpha.Number)) } - return newNumberFormulaArg((1 / (math.Pow(beta.Number, alpha.Number) * math.Gamma(alpha.Number))) * math.Pow(x.Number, (alpha.Number-1)) * math.Exp(0-(x.Number/beta.Number))) + return newNumberFormulaArg((1 / (math.Pow(beta.Number, alpha.Number) * math.Gamma(alpha.Number))) * math.Pow(x.Number, alpha.Number-1) * math.Exp(0-(x.Number/beta.Number))) } // gammainv returns the inverse of the Gamma distribution for the specified @@ -6797,17 +6800,17 @@ func gammainv(probability, alpha, beta float64) float64 { dx, x, xNew, result := 1024.0, 1.0, 1.0, 0.0 for i := 0; math.Abs(dx) > 8.88e-016 && i <= 256; i++ { result = incompleteGamma(alpha, x/beta) / math.Gamma(alpha) - error := result - probability - if error == 0 { + e := result - probability + if e == 0 { dx = 0 - } else if error < 0 { + } else if e < 0 { xLo = x } else { xHi = x } - pdf := (1 / (math.Pow(beta, alpha) * math.Gamma(alpha))) * math.Pow(x, (alpha-1)) * math.Exp(0-(x/beta)) + pdf := (1 / (math.Pow(beta, alpha) * math.Gamma(alpha))) * math.Pow(x, alpha-1) * math.Exp(0-(x/beta)) if pdf != 0 { - dx = error / pdf + dx = e / pdf xNew = x - dx } if xNew < xLo || xNew > xHi || pdf == 0 { @@ -6925,7 +6928,7 @@ func (fn *formulaFuncs) GEOMEAN(argsList *list.List) formulaArg { count := fn.COUNT(argsList) min := fn.MIN(argsList) if product.Number > 0 && min.Number > 0 { - return newNumberFormulaArg(math.Pow(product.Number, (1 / count.Number))) + return newNumberFormulaArg(math.Pow(product.Number, 1/count.Number)) } return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } @@ -6958,7 +6961,7 @@ func (fn *formulaFuncs) HARMEAN(argsList *list.List) formulaArg { if number <= 0 { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - val += (1 / number) + val += 1 / number cnt++ } return newNumberFormulaArg(1 / (val / cnt)) @@ -7206,7 +7209,7 @@ func (fn *formulaFuncs) LOGNORMdotDIST(argsList *list.List) formulaArg { return fn.NORMDIST(args) } return newNumberFormulaArg((1 / (math.Sqrt(2*math.Pi) * stdDev.Number * x.Number)) * - math.Exp(0-(math.Pow((math.Log(x.Number)-mean.Number), 2)/(2*math.Pow(stdDev.Number, 2))))) + math.Exp(0-(math.Pow(math.Log(x.Number)-mean.Number, 2)/(2*math.Pow(stdDev.Number, 2))))) } // LOGNORMDIST function calculates the Cumulative Log-Normal Distribution @@ -7455,7 +7458,7 @@ func (fn *formulaFuncs) kth(name string, argsList *list.List) formulaArg { if k < 1 { return newErrorFormulaArg(formulaErrorNUM, "k should be > 0") } - data := []float64{} + var data []float64 for _, arg := range array { if numArg := arg.ToNumber(); numArg.Type == ArgNumber { data = append(data, numArg.Number) @@ -7519,7 +7522,8 @@ func (fn *formulaFuncs) MAXIFS(argsList *list.List) formulaArg { if argsList.Len()%2 != 1 { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - max, maxRange, args := -math.MaxFloat64, argsList.Front().Value.(formulaArg).Matrix, []formulaArg{} + var args []formulaArg + max, maxRange := -math.MaxFloat64, argsList.Front().Value.(formulaArg).Matrix for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() { args = append(args, arg.Value.(formulaArg)) } @@ -7606,7 +7610,7 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "MEDIAN requires at least 1 argument") } - values := []float64{} + var values []float64 var median, digits float64 var err error for token := argsList.Front(); token != nil; token = token.Next() { @@ -7682,7 +7686,8 @@ func (fn *formulaFuncs) MINIFS(argsList *list.List) formulaArg { if argsList.Len()%2 != 1 { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - min, minRange, args := math.MaxFloat64, argsList.Front().Value.(formulaArg).Matrix, []formulaArg{} + var args []formulaArg + min, minRange := math.MaxFloat64, argsList.Front().Value.(formulaArg).Matrix for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() { args = append(args, arg.Value.(formulaArg)) } @@ -7778,7 +7783,7 @@ func (fn *formulaFuncs) PERCENTILEdotEXC(argsList *list.List) formulaArg { if k.Number <= 0 || k.Number >= 1 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - numbers := []float64{} + var numbers []float64 for _, arg := range array { if arg.Type == ArgError { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) @@ -7828,7 +7833,7 @@ func (fn *formulaFuncs) PERCENTILE(argsList *list.List) formulaArg { if k.Number < 0 || k.Number > 1 { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - numbers := []float64{} + var numbers []float64 for _, arg := range array { if arg.Type == ArgError { return arg @@ -7861,7 +7866,7 @@ func (fn *formulaFuncs) percentrank(name string, argsList *list.List) formulaArg if x.Type != ArgNumber { return x } - numbers := []float64{} + var numbers []float64 for _, arg := range array { if arg.Type == ArgError { return arg @@ -7895,10 +7900,10 @@ func (fn *formulaFuncs) percentrank(name string, argsList *list.List) formulaArg pos-- pos += (x.Number - numbers[int(pos)]) / (cmp - numbers[int(pos)]) } - pow := math.Pow(10, float64(significance.Number)) - digit := pow * float64(pos) / (float64(cnt) - 1) + pow := math.Pow(10, significance.Number) + digit := pow * pos / (float64(cnt) - 1) if name == "PERCENTRANK.EXC" { - digit = pow * float64(pos+1) / (float64(cnt) + 1) + digit = pow * (pos + 1) / (float64(cnt) + 1) } return newNumberFormulaArg(math.Floor(digit) / pow) } @@ -8049,7 +8054,7 @@ func (fn *formulaFuncs) rank(name string, argsList *list.List) formulaArg { if num.Type != ArgNumber { return num } - arr := []float64{} + var arr []float64 for _, arg := range argsList.Front().Next().Value.(formulaArg).ToList() { n := arg.ToNumber() if n.Type == ArgNumber { @@ -8072,7 +8077,7 @@ func (fn *formulaFuncs) rank(name string, argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } -// RANK.EQ function returns the statistical rank of a given value, within a +// RANKdotEQ function returns the statistical rank of a given value, within a // supplied array of values. If there are duplicate values in the list, these // are given the same rank. The syntax of the function is: // @@ -8212,7 +8217,7 @@ func (fn *formulaFuncs) TRIMMEAN(argsList *list.List) formulaArg { if percent.Number < 0 || percent.Number >= 1 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - arr := []float64{} + var arr []float64 arrArg := argsList.Front().Value.(formulaArg).ToList() for _, cell := range arrArg { num := cell.ToNumber() @@ -8254,14 +8259,14 @@ func (fn *formulaFuncs) vars(name string, argsList *list.List) formulaArg { for _, token := range arg.Value.(formulaArg).ToList() { num := token.ToNumber() if token.Value() != "TRUE" && num.Type == ArgNumber { - summerA += (num.Number * num.Number) + summerA += num.Number * num.Number summerB += num.Number count++ continue } num = token.ToBool() if num.Type == ArgNumber { - summerA += (num.Number * num.Number) + summerA += num.Number * num.Number summerB += num.Number count++ continue @@ -8352,10 +8357,10 @@ func (fn *formulaFuncs) WEIBULL(argsList *list.List) formulaArg { } cumulative := argsList.Back().Value.(formulaArg).ToBool() if cumulative.Boolean && cumulative.Number == 1 { - return newNumberFormulaArg(1 - math.Exp(0-math.Pow((x.Number/beta.Number), alpha.Number))) + return newNumberFormulaArg(1 - math.Exp(0-math.Pow(x.Number/beta.Number, alpha.Number))) } return newNumberFormulaArg((alpha.Number / math.Pow(beta.Number, alpha.Number)) * - math.Pow(x.Number, (alpha.Number-1)) * math.Exp(0-math.Pow((x.Number/beta.Number), alpha.Number))) + math.Pow(x.Number, alpha.Number-1) * math.Exp(0-math.Pow(x.Number/beta.Number, alpha.Number))) } return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } @@ -8612,7 +8617,7 @@ func (fn *formulaFuncs) ISNA(argsList *list.List) formulaArg { return newStringFormulaArg(result) } -// ISNONTEXT function function tests if a supplied value is text. If not, the +// ISNONTEXT function tests if a supplied value is text. If not, the // function returns TRUE; If the supplied value is text, the function returns // FALSE. The syntax of the function is: // @@ -8630,7 +8635,7 @@ func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) formulaArg { return newStringFormulaArg(result) } -// ISNUMBER function function tests if a supplied value is a number. If so, +// ISNUMBER function tests if a supplied value is a number. If so, // the function returns TRUE; Otherwise it returns FALSE. The syntax of the // function is: // @@ -8893,7 +8898,7 @@ func (fn *formulaFuncs) AND(argsList *list.List) formulaArg { return newBoolFormulaArg(and) } -// FALSE function function returns the logical value FALSE. The syntax of the +// FALSE function returns the logical value FALSE. The syntax of the // function is: // // FALSE() @@ -9221,16 +9226,16 @@ func (fn *formulaFuncs) DATEDIF(argsList *list.List) formulaArg { diff-- } case "m": - ydiff := ey - sy - mdiff := em - sm + yDiff := ey - sy + mDiff := em - sm if ed < sd { - mdiff-- + mDiff-- } - if mdiff < 0 { - ydiff-- - mdiff += 12 + if mDiff < 0 { + yDiff-- + mDiff += 12 } - diff = float64(ydiff*12 + mdiff) + diff = float64(yDiff*12 + mDiff) case "d", "md", "ym", "yd": diff = calcDateDif(unit, diff, []int{ey, sy, em, sm, ed, sd}, startArg, endArg) default: @@ -9242,8 +9247,8 @@ func (fn *formulaFuncs) DATEDIF(argsList *list.List) formulaArg { // isDateOnlyFmt check if the given string matches date-only format regular expressions. func isDateOnlyFmt(dateString string) bool { for _, df := range dateOnlyFormats { - submatch := df.FindStringSubmatch(dateString) - if len(submatch) > 1 { + subMatch := df.FindStringSubmatch(dateString) + if len(subMatch) > 1 { return true } } @@ -9253,8 +9258,8 @@ func isDateOnlyFmt(dateString string) bool { // isTimeOnlyFmt check if the given string matches time-only format regular expressions. func isTimeOnlyFmt(timeString string) bool { for _, tf := range timeFormats { - submatch := tf.FindStringSubmatch(timeString) - if len(submatch) > 1 { + subMatch := tf.FindStringSubmatch(timeString) + if len(subMatch) > 1 { return true } } @@ -9263,50 +9268,51 @@ func isTimeOnlyFmt(timeString string) bool { // strToTimePatternHandler1 parse and convert the given string in pattern // hh to the time. -func strToTimePatternHandler1(submatch []string) (h, m int, s float64, err error) { - h, err = strconv.Atoi(submatch[0]) +func strToTimePatternHandler1(subMatch []string) (h, m int, s float64, err error) { + h, err = strconv.Atoi(subMatch[0]) return } // strToTimePatternHandler2 parse and convert the given string in pattern // hh:mm to the time. -func strToTimePatternHandler2(submatch []string) (h, m int, s float64, err error) { - if h, err = strconv.Atoi(submatch[0]); err != nil { +func strToTimePatternHandler2(subMatch []string) (h, m int, s float64, err error) { + if h, err = strconv.Atoi(subMatch[0]); err != nil { return } - m, err = strconv.Atoi(submatch[2]) + m, err = strconv.Atoi(subMatch[2]) return } // strToTimePatternHandler3 parse and convert the given string in pattern // mm:ss to the time. -func strToTimePatternHandler3(submatch []string) (h, m int, s float64, err error) { - if m, err = strconv.Atoi(submatch[0]); err != nil { +func strToTimePatternHandler3(subMatch []string) (h, m int, s float64, err error) { + if m, err = strconv.Atoi(subMatch[0]); err != nil { return } - s, err = strconv.ParseFloat(submatch[2], 64) + s, err = strconv.ParseFloat(subMatch[2], 64) return } // strToTimePatternHandler4 parse and convert the given string in pattern // hh:mm:ss to the time. -func strToTimePatternHandler4(submatch []string) (h, m int, s float64, err error) { - if h, err = strconv.Atoi(submatch[0]); err != nil { +func strToTimePatternHandler4(subMatch []string) (h, m int, s float64, err error) { + if h, err = strconv.Atoi(subMatch[0]); err != nil { return } - if m, err = strconv.Atoi(submatch[2]); err != nil { + if m, err = strconv.Atoi(subMatch[2]); err != nil { return } - s, err = strconv.ParseFloat(submatch[4], 64) + s, err = strconv.ParseFloat(subMatch[4], 64) return } // strToTime parse and convert the given string to the time. func strToTime(str string) (int, int, float64, bool, bool, formulaArg) { - pattern, submatch := "", []string{} + var subMatch []string + pattern := "" for key, tf := range timeFormats { - submatch = tf.FindStringSubmatch(str) - if len(submatch) > 1 { + subMatch = tf.FindStringSubmatch(str) + if len(subMatch) > 1 { pattern = key break } @@ -9314,24 +9320,24 @@ func strToTime(str string) (int, int, float64, bool, bool, formulaArg) { if pattern == "" { return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - dateIsEmpty := submatch[1] == "" - submatch = submatch[49:] + dateIsEmpty := subMatch[1] == "" + subMatch = subMatch[49:] var ( - l = len(submatch) - last = submatch[l-1] + l = len(subMatch) + last = subMatch[l-1] am = last == "am" pm = last == "pm" hours, minutes int seconds float64 err error ) - if handler, ok := map[string]func(subsubmatch []string) (int, int, float64, error){ + if handler, ok := map[string]func(match []string) (int, int, float64, error){ "hh": strToTimePatternHandler1, "hh:mm": strToTimePatternHandler2, "mm:ss": strToTimePatternHandler3, "hh:mm:ss": strToTimePatternHandler4, }[pattern]; ok { - if hours, minutes, seconds, err = handler(submatch); err != nil { + if hours, minutes, seconds, err = handler(subMatch); err != nil { return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } } @@ -9352,55 +9358,55 @@ func strToTime(str string) (int, int, float64, bool, bool, formulaArg) { // strToDatePatternHandler1 parse and convert the given string in pattern // mm/dd/yy to the date. -func strToDatePatternHandler1(submatch []string) (int, int, int, bool, error) { +func strToDatePatternHandler1(subMatch []string) (int, int, int, bool, error) { var year, month, day int var err error - if month, err = strconv.Atoi(submatch[1]); err != nil { + if month, err = strconv.Atoi(subMatch[1]); err != nil { return 0, 0, 0, false, err } - if day, err = strconv.Atoi(submatch[3]); err != nil { + if day, err = strconv.Atoi(subMatch[3]); err != nil { return 0, 0, 0, false, err } - if year, err = strconv.Atoi(submatch[5]); err != nil { + if year, err = strconv.Atoi(subMatch[5]); err != nil { return 0, 0, 0, false, err } if year < 0 || year > 9999 || (year > 99 && year < 1900) { return 0, 0, 0, false, ErrParameterInvalid } - return formatYear(year), month, day, submatch[8] == "", err + return formatYear(year), month, day, subMatch[8] == "", err } // strToDatePatternHandler2 parse and convert the given string in pattern mm // dd, yy to the date. -func strToDatePatternHandler2(submatch []string) (int, int, int, bool, error) { +func strToDatePatternHandler2(subMatch []string) (int, int, int, bool, error) { var year, month, day int var err error - month = month2num[submatch[1]] - if day, err = strconv.Atoi(submatch[14]); err != nil { + month = month2num[subMatch[1]] + if day, err = strconv.Atoi(subMatch[14]); err != nil { return 0, 0, 0, false, err } - if year, err = strconv.Atoi(submatch[16]); err != nil { + if year, err = strconv.Atoi(subMatch[16]); err != nil { return 0, 0, 0, false, err } if year < 0 || year > 9999 || (year > 99 && year < 1900) { return 0, 0, 0, false, ErrParameterInvalid } - return formatYear(year), month, day, submatch[19] == "", err + return formatYear(year), month, day, subMatch[19] == "", err } // strToDatePatternHandler3 parse and convert the given string in pattern // yy-mm-dd to the date. -func strToDatePatternHandler3(submatch []string) (int, int, int, bool, error) { +func strToDatePatternHandler3(subMatch []string) (int, int, int, bool, error) { var year, month, day int - v1, err := strconv.Atoi(submatch[1]) + v1, err := strconv.Atoi(subMatch[1]) if err != nil { return 0, 0, 0, false, err } - v2, err := strconv.Atoi(submatch[3]) + v2, err := strconv.Atoi(subMatch[3]) if err != nil { return 0, 0, 0, false, err } - v3, err := strconv.Atoi(submatch[5]) + v3, err := strconv.Atoi(subMatch[5]) if err != nil { return 0, 0, 0, false, err } @@ -9415,30 +9421,31 @@ func strToDatePatternHandler3(submatch []string) (int, int, int, bool, error) { } else { return 0, 0, 0, false, ErrParameterInvalid } - return year, month, day, submatch[8] == "", err + return year, month, day, subMatch[8] == "", err } // strToDatePatternHandler4 parse and convert the given string in pattern // yy-mmStr-dd, yy to the date. -func strToDatePatternHandler4(submatch []string) (int, int, int, bool, error) { +func strToDatePatternHandler4(subMatch []string) (int, int, int, bool, error) { var year, month, day int var err error - if year, err = strconv.Atoi(submatch[16]); err != nil { + if year, err = strconv.Atoi(subMatch[16]); err != nil { return 0, 0, 0, false, err } - month = month2num[submatch[3]] - if day, err = strconv.Atoi(submatch[1]); err != nil { + month = month2num[subMatch[3]] + if day, err = strconv.Atoi(subMatch[1]); err != nil { return 0, 0, 0, false, err } - return formatYear(year), month, day, submatch[19] == "", err + return formatYear(year), month, day, subMatch[19] == "", err } // strToDate parse and convert the given string to the date. func strToDate(str string) (int, int, int, bool, formulaArg) { - pattern, submatch := "", []string{} + var subMatch []string + pattern := "" for key, df := range dateFormats { - submatch = df.FindStringSubmatch(str) - if len(submatch) > 1 { + subMatch = df.FindStringSubmatch(str) + if len(subMatch) > 1 { pattern = key break } @@ -9451,13 +9458,13 @@ func strToDate(str string) (int, int, int, bool, formulaArg) { year, month, day int err error ) - if handler, ok := map[string]func(subsubmatch []string) (int, int, int, bool, error){ + if handler, ok := map[string]func(match []string) (int, int, int, bool, error){ "mm/dd/yy": strToDatePatternHandler1, "mm dd, yy": strToDatePatternHandler2, "yy-mm-dd": strToDatePatternHandler3, "yy-mmStr-dd": strToDatePatternHandler4, }[pattern]; ok { - if year, month, day, timeIsEmpty, err = handler(submatch); err != nil { + if year, month, day, timeIsEmpty, err = handler(subMatch); err != nil { return 0, 0, 0, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } } @@ -9469,8 +9476,8 @@ func strToDate(str string) (int, int, int, bool, formulaArg) { // DATEVALUE function converts a text representation of a date into an Excel // date. For example, the function converts a text string representing a -// date, into the serial number that represents the date in Excel's date-time -// code. The syntax of the function is: +// date, into the serial number that represents the date in Excels' date-time +// code. The syntax of the function is: // // DATEVALUE(date_text) // @@ -9553,7 +9560,7 @@ func (fn *formulaFuncs) ISOWEEKNUM(argsList *list.List) formulaArg { } date := argsList.Front().Value.(formulaArg) num := date.ToNumber() - weeknum := 0 + weekNum := 0 if num.Type != ArgNumber { dateString := strings.ToLower(date.Value()) if !isDateOnlyFmt(dateString) { @@ -9565,14 +9572,14 @@ func (fn *formulaFuncs) ISOWEEKNUM(argsList *list.List) formulaArg { if err.Type == ArgError { return err } - _, weeknum = time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC).ISOWeek() + _, weekNum = time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC).ISOWeek() } else { if num.Number < 0 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - _, weeknum = timeFromExcelTime(num.Number, false).ISOWeek() + _, weekNum = timeFromExcelTime(num.Number, false).ISOWeek() } - return newNumberFormulaArg(float64(weeknum)) + return newNumberFormulaArg(float64(weekNum)) } // HOUR function returns an integer representing the hour component of a @@ -9889,7 +9896,7 @@ func (fn *formulaFuncs) SECOND(argsList *list.List) formulaArg { // TIME function accepts three integer arguments representing hours, minutes // and seconds, and returns an Excel time. I.e. the function returns the -// decimal value that represents the time in Excel. The syntax of the Time +// decimal value that represents the time in Excel. The syntax of the // function is: // // TIME(hour,minute,second) @@ -9912,7 +9919,7 @@ func (fn *formulaFuncs) TIME(argsList *list.List) formulaArg { } // TIMEVALUE function converts a text representation of a time, into an Excel -// time. The syntax of the Timevalue function is: +// time. The syntax of the function is: // // TIMEVALUE(time_text) // @@ -10875,19 +10882,18 @@ func matchPattern(pattern, name string) (matched bool) { if pattern == "*" { return true } - rname, rpattern := make([]rune, 0, len(name)), make([]rune, 0, len(pattern)) + rName, rPattern := make([]rune, 0, len(name)), make([]rune, 0, len(pattern)) for _, r := range name { - rname = append(rname, r) + rName = append(rName, r) } for _, r := range pattern { - rpattern = append(rpattern, r) + rPattern = append(rPattern, r) } - simple := false // Does extended wildcard '*' and '?' match. - return deepMatchRune(rname, rpattern, simple) + return deepMatchRune(rName, rPattern, false) } -// compareFormulaArg compares the left-hand sides and the right-hand sides -// formula arguments by given conditions such as case sensitive, if exact +// compareFormulaArg compares the left-hand sides and the right-hand sides' +// formula arguments by given conditions such as case-sensitive, if exact // match, and make compare result as formula criteria condition type. func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte { if lhs.Type != rhs.Type { @@ -10941,7 +10947,7 @@ func compareFormulaArgList(lhs, rhs, matchMode formulaArg, caseSensitive bool) b return criteriaEq } -// compareFormulaArgMatrix compares the left-hand sides and the right-hand sides +// compareFormulaArgMatrix compares the left-hand sides and the right-hand sides' // matrix type formula arguments. func compareFormulaArgMatrix(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte { if len(lhs.Matrix) < len(rhs.Matrix) { @@ -11267,7 +11273,7 @@ func (fn *formulaFuncs) TRANSPOSE(argsList *list.List) formulaArg { // lookupLinearSearch sequentially checks each look value of the lookup array until // a match is found or the whole list has been searched. func lookupLinearSearch(vertical bool, lookupValue, lookupArray, matchMode, searchMode formulaArg) (int, bool) { - tableArray := []formulaArg{} + var tableArray []formulaArg if vertical { for _, row := range lookupArray.Matrix { tableArray = append(tableArray, row[0]) @@ -11336,7 +11342,7 @@ func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg { // is TRUE, if the data of table array can't guarantee be sorted, it will // return wrong result. func lookupBinarySearch(vertical bool, lookupValue, lookupArray, matchMode, searchMode formulaArg) (matchIdx int, wasExact bool) { - tableArray := []formulaArg{} + var tableArray []formulaArg if vertical { for _, row := range lookupArray.Matrix { tableArray = append(tableArray, row[0]) @@ -11344,7 +11350,7 @@ func lookupBinarySearch(vertical bool, lookupValue, lookupArray, matchMode, sear } else { tableArray = lookupArray.Matrix[0] } - var low, high, lastMatchIdx int = 0, len(tableArray) - 1, -1 + var low, high, lastMatchIdx = 0, len(tableArray) - 1, -1 count := high for low <= high { mid := low + (high-low)/2 @@ -11425,7 +11431,7 @@ func iterateLookupArgs(lookupValue, lookupVector formulaArg) ([]formulaArg, int, matchIdx = idx break } - // Find nearest match if lookup value is more than or equal to the first value in lookup vector + // Find the nearest match if lookup value is more than or equal to the first value in lookup vector if idx == 0 { ok = compare == criteriaG } else if ok && compare == criteriaL && matchIdx == -1 { @@ -11447,7 +11453,7 @@ func (fn *formulaFuncs) index(array formulaArg, rowIdx, colIdx int) formulaArg { if colIdx >= len(cellMatrix[0]) { return newErrorFormulaArg(formulaErrorREF, "INDEX col_num out of range") } - column := [][]formulaArg{} + var column [][]formulaArg for _, cells = range cellMatrix { column = append(column, []formulaArg{cells[colIdx]}) } @@ -11513,7 +11519,7 @@ func (fn *formulaFuncs) prepareXlookupArgs(argsList *list.List) formulaArg { func (fn *formulaFuncs) xlookup(lookupRows, lookupCols, returnArrayRows, returnArrayCols, matchIdx int, condition1, condition2, condition3, condition4 bool, returnArray formulaArg, ) formulaArg { - result := [][]formulaArg{} + var result [][]formulaArg for rowIdx, row := range returnArray.Matrix { for colIdx, cell := range row { if condition1 { @@ -12105,7 +12111,7 @@ func is30BasisMethod(basis int) bool { // getDaysInMonthRange return the day by given year, month range and day count // basis. -func getDaysInMonthRange(year, fromMonth, toMonth, basis int) int { +func getDaysInMonthRange(fromMonth, toMonth int) int { if fromMonth > toMonth { return 0 } @@ -12153,10 +12159,10 @@ func coupdays(from, to time.Time, basis int) float64 { fromDay = 1 date := time.Date(fromY, fromM, fromD, 0, 0, 0, 0, time.UTC).AddDate(0, 1, 0) if date.Year() < toY { - days += getDaysInMonthRange(date.Year(), int(date.Month()), 12, basis) + days += getDaysInMonthRange(int(date.Month()), 12) date = date.AddDate(0, 13-int(date.Month()), 0) } - days += getDaysInMonthRange(toY, int(date.Month()), int(toM)-1, basis) + days += getDaysInMonthRange(int(date.Month()), int(toM)-1) } if days += toDay - fromDay; days > 0 { return float64(days) @@ -12363,7 +12369,7 @@ func (fn *formulaFuncs) cumip(name string, argsList *list.List) formulaArg { return newNumberFormulaArg(num) } -// calcDbArgsCompare implements common arguments comparison for DB and DDB. +// calcDbArgsCompare implements common arguments' comparison for DB and DDB. func calcDbArgsCompare(cost, salvage, life, period formulaArg) bool { return (cost.Number <= 0) || ((salvage.Number / cost.Number) < 0) || (life.Number <= 0) || (period.Number < 1) } @@ -12468,7 +12474,7 @@ func (fn *formulaFuncs) DDB(argsList *list.List) formulaArg { } pd, depreciation := 0.0, 0.0 for per := 1; per <= int(period.Number); per++ { - depreciation = math.Min((cost.Number-pd)*(factor.Number/life.Number), (cost.Number - salvage.Number - pd)) + depreciation = math.Min((cost.Number-pd)*(factor.Number/life.Number), cost.Number-salvage.Number-pd) pd += depreciation } return newNumberFormulaArg(depreciation) @@ -12478,7 +12484,7 @@ func (fn *formulaFuncs) DDB(argsList *list.List) formulaArg { // formula functions. func (fn *formulaFuncs) prepareDataValueArgs(n int, argsList *list.List) formulaArg { l := list.New() - dataValues := []formulaArg{} + var dataValues []formulaArg getDateValue := func(arg formulaArg, l *list.List) formulaArg { switch arg.Type { case ArgNumber: @@ -12715,7 +12721,7 @@ func (fn *formulaFuncs) EFFECT(argsList *list.List) formulaArg { if rate.Number <= 0 || npery.Number < 1 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newNumberFormulaArg(math.Pow((1+rate.Number/npery.Number), npery.Number) - 1) + return newNumberFormulaArg(math.Pow(1+rate.Number/npery.Number, npery.Number) - 1) } // FV function calculates the Future Value of an investment with periodic @@ -12785,7 +12791,7 @@ func (fn *formulaFuncs) FVSCHEDULE(argsList *list.List) formulaArg { if rate.Type != ArgNumber { return rate } - principal *= (1 + rate.Number) + principal *= 1 + rate.Number } return newNumberFormulaArg(principal) } @@ -12945,7 +12951,7 @@ func (fn *formulaFuncs) IRR(argsList *list.List) formulaArg { if f1.Number*f2.Number < 0 { break } - if math.Abs(f1.Number) < math.Abs((f2.Number)) { + if math.Abs(f1.Number) < math.Abs(f2.Number) { x1.Number += 1.6 * (x1.Number - x2.Number) args.Front().Value = x1 f1 = fn.NPV(args) @@ -13061,10 +13067,10 @@ func (fn *formulaFuncs) MIRR(argsList *list.List) formulaArg { for i, v := range values { val := v.ToNumber() if val.Number >= 0 { - npvPos += val.Number / math.Pow(float64(rr), float64(i)) + npvPos += val.Number / math.Pow(rr, float64(i)) continue } - npvNeg += val.Number / math.Pow(float64(fr), float64(i)) + npvNeg += val.Number / math.Pow(fr, float64(i)) } if npvNeg == 0 || npvPos == 0 || reinvestRate.Number <= -1 { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) @@ -13173,7 +13179,7 @@ func (fn *formulaFuncs) NPV(argsList *list.List) formulaArg { // aggrBetween is a part of implementation of the formula function ODDFPRICE. func aggrBetween(startPeriod, endPeriod float64, initialValue []float64, f func(acc []float64, index float64) []float64) []float64 { - s := []float64{} + var s []float64 if startPeriod <= endPeriod { for i := startPeriod; i <= endPeriod; i++ { s = append(s, i) @@ -13211,7 +13217,7 @@ func changeMonth(date time.Time, numMonths float64, returnLastMonth bool) time.T // datesAggregate is a part of implementation of the formula function // ODDFPRICE. -func datesAggregate(startDate, endDate time.Time, numMonths, basis float64, f func(pcd, ncd time.Time) float64, acc float64, returnLastMonth bool) (time.Time, time.Time, float64) { +func datesAggregate(startDate, endDate time.Time, numMonths float64, f func(pcd, ncd time.Time) float64, acc float64, returnLastMonth bool) (time.Time, time.Time, float64) { frontDate, trailingDate := startDate, endDate s1 := frontDate.After(endDate) || frontDate.Equal(endDate) s2 := endDate.After(frontDate) || endDate.Equal(frontDate) @@ -13235,7 +13241,7 @@ func datesAggregate(startDate, endDate time.Time, numMonths, basis float64, f fu } // coupNumber is a part of implementation of the formula function ODDFPRICE. -func coupNumber(maturity, settlement, numMonths, basis float64) float64 { +func coupNumber(maturity, settlement, numMonths float64) float64 { maturityTime, settlementTime := timeFromExcelTime(maturity, false), timeFromExcelTime(settlement, false) my, mm, md := maturityTime.Year(), maturityTime.Month(), maturityTime.Day() sy, sm, sd := settlementTime.Year(), settlementTime.Month(), settlementTime.Day() @@ -13253,7 +13259,7 @@ func coupNumber(maturity, settlement, numMonths, basis float64) float64 { f := func(pcd, ncd time.Time) float64 { return 1 } - _, _, result := datesAggregate(date, maturityTime, numMonths, basis, f, coupons, endOfMonth) + _, _, result := datesAggregate(date, maturityTime, numMonths, f, coupons, endOfMonth) return result } @@ -13338,7 +13344,7 @@ func (fn *formulaFuncs) ODDFPRICE(argsList *list.List) formulaArg { numMonths := 12 / frequency.Number numMonthsNeg := -numMonths mat := changeMonth(maturityTime, numMonthsNeg, returnLastMonth) - pcd, _, _ := datesAggregate(mat, firstCouponTime, numMonthsNeg, basisArg.Number, func(d1, d2 time.Time) float64 { + pcd, _, _ := datesAggregate(mat, firstCouponTime, numMonthsNeg, func(d1, d2 time.Time) float64 { return 0 }, 0, returnLastMonth) if !pcd.Equal(firstCouponTime) { @@ -13419,7 +13425,7 @@ func (fn *formulaFuncs) ODDFPRICE(argsList *list.List) formulaArg { a := coupdays(d, settlementTime, basis) dsc = e.Number - a } - nq := coupNumber(firstCoupon.Number, settlement.Number, numMonths, basisArg.Number) + nq := coupNumber(firstCoupon.Number, settlement.Number, numMonths) fnArgs.Init() fnArgs.PushBack(firstCoupon) fnArgs.PushBack(maturity) @@ -13508,7 +13514,7 @@ func (fn *formulaFuncs) PMT(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } if rate.Number != 0 { - p := (-fv.Number - pv.Number*math.Pow((1+rate.Number), nper.Number)) / (1 + rate.Number*typ.Number) / ((math.Pow((1+rate.Number), nper.Number) - 1) / rate.Number) + p := (-fv.Number - pv.Number*math.Pow(1+rate.Number, nper.Number)) / (1 + rate.Number*typ.Number) / ((math.Pow(1+rate.Number, nper.Number) - 1) / rate.Number) return newNumberFormulaArg(p) } return newNumberFormulaArg((-pv.Number - fv.Number) / nper.Number) @@ -13747,9 +13753,9 @@ func (fn *formulaFuncs) PV(argsList *list.List) formulaArg { } // rate is an implementation of the formula function RATE. -func (fn *formulaFuncs) rate(nper, pmt, pv, fv, t, guess formulaArg, argsList *list.List) formulaArg { - maxIter, iter, close, epsMax, rate := 100, 0, false, 1e-6, guess.Number - for iter < maxIter && !close { +func (fn *formulaFuncs) rate(nper, pmt, pv, fv, t, guess formulaArg) formulaArg { + maxIter, iter, isClose, epsMax, rate := 100, 0, false, 1e-6, guess.Number + for iter < maxIter && !isClose { t1 := math.Pow(rate+1, nper.Number) t2 := math.Pow(rate+1, nper.Number-1) rt := rate*t.Number + 1 @@ -13761,7 +13767,7 @@ func (fn *formulaFuncs) rate(nper, pmt, pv, fv, t, guess formulaArg, argsList *l f3 := (nper.Number*pmt.Number*t2*rt + p0*t.Number) / rate delta := f1 / (f2 + f3) if math.Abs(delta) < epsMax { - close = true + isClose = true } iter++ rate -= delta @@ -13815,7 +13821,7 @@ func (fn *formulaFuncs) RATE(argsList *list.List) formulaArg { return guess } } - return fn.rate(nper, pmt, pv, fv, t, guess, argsList) + return fn.rate(nper, pmt, pv, fv, t, guess) } // RECEIVED function calculates the amount received at maturity for a fully @@ -14159,7 +14165,7 @@ func (fn *formulaFuncs) VDB(argsList *list.List) formulaArg { } // prepareXArgs prepare arguments for the formula function XIRR and XNPV. -func (fn *formulaFuncs) prepareXArgs(name string, values, dates formulaArg) (valuesArg, datesArg []float64, err formulaArg) { +func (fn *formulaFuncs) prepareXArgs(values, dates formulaArg) (valuesArg, datesArg []float64, err formulaArg) { for _, arg := range values.ToList() { if numArg := arg.ToNumber(); numArg.Type == ArgNumber { valuesArg = append(valuesArg, numArg.Number) @@ -14267,7 +14273,7 @@ func (fn *formulaFuncs) XIRR(argsList *list.List) formulaArg { if argsList.Len() != 2 && argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "XIRR requires 2 or 3 arguments") } - values, dates, err := fn.prepareXArgs("XIRR", argsList.Front().Value.(formulaArg), argsList.Front().Next().Value.(formulaArg)) + values, dates, err := fn.prepareXArgs(argsList.Front().Value.(formulaArg), argsList.Front().Next().Value.(formulaArg)) if err.Type != ArgEmpty { return err } @@ -14299,7 +14305,7 @@ func (fn *formulaFuncs) XNPV(argsList *list.List) formulaArg { if rate.Number <= 0 { return newErrorFormulaArg(formulaErrorVALUE, "XNPV requires rate > 0") } - values, dates, err := fn.prepareXArgs("XNPV", argsList.Front().Next().Value.(formulaArg), argsList.Back().Value.(formulaArg)) + values, dates, err := fn.prepareXArgs(argsList.Front().Next().Value.(formulaArg), argsList.Back().Value.(formulaArg)) if err.Type != ArgEmpty { return err } diff --git a/calc_test.go b/calc_test.go index 4025ceca11..6708cdbbbb 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4629,7 +4629,7 @@ func TestCalcLogBeta(t *testing.T) { } func TestCalcBetainvProbIterator(t *testing.T) { - assert.Equal(t, 1.0, betainvProbIterator(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, true)) + assert.Equal(t, 1.0, betainvProbIterator(1, 1, 1, 1, 1, 1, 1, 1, 1)) } func TestNestedFunctionsWithOperators(t *testing.T) { diff --git a/cell.go b/cell.go index 6ecce4febe..4b76271a70 100644 --- a/cell.go +++ b/cell.go @@ -200,7 +200,7 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, axis) if err != nil { return err } @@ -251,7 +251,7 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, axis) if err != nil { return err } @@ -276,7 +276,7 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, axis) if err != nil { return err } @@ -299,7 +299,7 @@ func setCellBool(value bool) (t string, v string) { return } -// SetCellFloat sets a floating point value into a cell. The prec parameter +// SetCellFloat sets a floating point value into a cell. The precision parameter // specifies how many places after the decimal will be shown while -1 is a // special value that will use as many decimal places as necessary to // represent the number. bitSize is 32 or 64 depending on if a float32 or @@ -308,26 +308,26 @@ func setCellBool(value bool) (t string, v string) { // var x float32 = 1.325 // f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) // -func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int) error { +func (f *File) SetCellFloat(sheet, axis string, value float64, precision, bitSize int) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, axis) if err != nil { return err } ws.Lock() defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) - cellData.T, cellData.V = setCellFloat(value, prec, bitSize) + cellData.T, cellData.V = setCellFloat(value, precision, bitSize) return err } // setCellFloat prepares cell type and string type cell value by a given // float value. -func setCellFloat(value float64, prec, bitSize int) (t string, v string) { - v = strconv.FormatFloat(value, 'f', prec, bitSize) +func setCellFloat(value float64, precision, bitSize int) (t string, v string) { + v = strconv.FormatFloat(value, 'f', precision, bitSize) return } @@ -338,7 +338,7 @@ func (f *File) SetCellStr(sheet, axis, value string) error { if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, axis) if err != nil { return err } @@ -436,7 +436,7 @@ func (f *File) SetCellDefault(sheet, axis, value string) error { if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, axis) if err != nil { return err } @@ -478,7 +478,7 @@ type FormulaOpts struct { } // SetCellFormula provides a function to set formula on the cell is taken -// according to the given worksheet name (case sensitive) and cell formula +// according to the given worksheet name (case-sensitive) and cell formula // settings. The result of the formula cell can be calculated when the // worksheet is opened by the Office Excel application or can be using // the "CalcCellValue" function also can get the calculated cell value. If @@ -560,7 +560,7 @@ func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) if err != nil { return err } - cellData, _, _, err := f.prepareCell(ws, sheet, axis) + cellData, _, _, err := f.prepareCell(ws, axis) if err != nil { return err } @@ -672,12 +672,9 @@ type HyperlinkOpts struct { // SetCellHyperLink provides a function to set cell hyperlink by given // worksheet name and link URL address. LinkType defines two types of -// hyperlink "External" for web site or "Location" for moving to one of cell -// in this workbook. Maximum limit hyperlinks in a worksheet is 65530. This -// function is only used to set the hyperlink of the cell and doesn't affect -// the value of the cell. If you need to set the value of the cell, please use -// the other functions such as `SetCellStyle` or `SetSheetRow`. The below is -// example for external link. +// hyperlink "External" for website or "Location" for moving to one of cell +// in this workbook. Maximum limit hyperlinks in a worksheet is 65530. The +// below is example for external link. // // if err := f.SetCellHyperLink("Sheet1", "A3", // "https://github.com/xuri/excelize", "External"); err != nil { @@ -692,7 +689,7 @@ type HyperlinkOpts struct { // } // err = f.SetCellStyle("Sheet1", "A3", "A3", style) // -// A this is another example for "Location": +// This is another example for "Location": // // err := f.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") // @@ -759,7 +756,7 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro if err != nil { return } - cellData, _, _, err := f.prepareCell(ws, sheet, cell) + cellData, _, _, err := f.prepareCell(ws, cell) if err != nil { return } @@ -940,7 +937,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, sheet, cell) + cellData, col, row, err := f.prepareCell(ws, cell) if err != nil { return err } @@ -950,7 +947,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) si := xlsxSI{} sst := f.sharedStringsReader() - textRuns := []xlsxR{} + var textRuns []xlsxR totalCellChars := 0 for _, textRun := range runs { totalCellChars += len(textRun.Text) @@ -1000,8 +997,8 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { for i := 0; i < v.Len(); i++ { cell, err := CoordinatesToCellName(col+i, row) - // Error should never happens here. But keep checking to early detect regresions - // if it will be introduced in future. + // Error should never happen here. But keep checking to early detect regressions + // if it will be introduced in the future. if err != nil { return err } @@ -1013,7 +1010,7 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { } // getCellInfo does common preparation for all SetCell* methods. -func (f *File) prepareCell(ws *xlsxWorksheet, sheet, cell string) (*xlsxC, int, int, error) { +func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, error) { var err error cell, err = f.mergeCellsParser(ws, cell) if err != nil { @@ -1175,7 +1172,7 @@ func (f *File) checkCellInArea(cell, area string) (bool, error) { return cellInRef([]int{col, row}, coordinates), err } -// cellInRef provides a function to determine if a given range is within an +// cellInRef provides a function to determine if a given range is within a // range. func cellInRef(cell, ref []int) bool { return cell[0] >= ref[0] && cell[0] <= ref[2] && cell[1] >= ref[1] && cell[1] <= ref[3] @@ -1241,7 +1238,7 @@ func parseSharedFormula(dCol, dRow int, orig []byte) (res string, start int) { // considered to be the same when their respective representations in // R1C1-reference notation, are the same. // -// Note that this function not validate ref tag to check the cell if or not in +// Note that this function not validate ref tag to check the cell whether in // allow area, and always return origin shared formula. func getSharedFormula(ws *xlsxWorksheet, si int, axis string) string { for _, r := range ws.SheetData.Row { @@ -1264,7 +1261,7 @@ func getSharedFormula(ws *xlsxWorksheet, si int, axis string) string { } // shiftCell returns the cell shifted according to dCol and dRow taking into -// consideration of absolute references with dollar sign ($) +// consideration absolute references with dollar sign ($) func shiftCell(cellID string, dCol, dRow int) string { fCol, fRow, _ := CellNameToCoordinates(cellID) signCol, signRow := "", "" diff --git a/cell_test.go b/cell_test.go index 92d3d2fdcc..73b3018b3f 100644 --- a/cell_test.go +++ b/cell_test.go @@ -33,7 +33,7 @@ func TestConcurrency(t *testing.T) { assert.NoError(t, f.SetSheetRow("Sheet1", "B6", &[]interface{}{ " Hello", []byte("World"), 42, int8(1<<8/2 - 1), int16(1<<16/2 - 1), int32(1<<32/2 - 1), - int64(1<<32/2 - 1), float32(42.65418), float64(-42.65418), float32(42), float64(42), + int64(1<<32/2 - 1), float32(42.65418), -42.65418, float32(42), float64(42), uint(1<<32 - 1), uint8(1<<8 - 1), uint16(1<<16 - 1), uint32(1<<32 - 1), uint64(1<<32 - 1), true, complex64(5 + 10i), })) diff --git a/chart.go b/chart.go index 4543770d01..8f521fa048 100644 --- a/chart.go +++ b/chart.go @@ -969,7 +969,7 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { // getFormatChart provides a function to check format set of the chart and // create chart format. func (f *File) getFormatChart(format string, combo []string) (*formatChart, []*formatChart, error) { - comboCharts := []*formatChart{} + var comboCharts []*formatChart formatSet, err := parseFormatChartSet(format) if err != nil { return formatSet, comboCharts, err diff --git a/chart_test.go b/chart_test.go index b1b4791f29..9f2287e802 100644 --- a/chart_test.go +++ b/chart_test.go @@ -353,7 +353,7 @@ func TestChartWithLogarithmicBase(t *testing.T) { } assert.True(t, ok, "Can't open the %s", chartPath) - err = xml.Unmarshal([]byte(xmlCharts[i]), &chartSpaces[i]) + err = xml.Unmarshal(xmlCharts[i], &chartSpaces[i]) if !assert.NoError(t, err) { t.FailNow() } diff --git a/col.go b/col.go index 827d7273b5..ee1a4074d5 100644 --- a/col.go +++ b/col.go @@ -40,8 +40,7 @@ type Cols struct { sheetXML []byte } -// GetCols return all the columns in a sheet by given worksheet name (case -// sensitive). For example: +// GetCols return all the columns in a sheet by given worksheet name (case-sensitive). For example: // // cols, err := f.GetCols("Sheet1") // if err != nil { @@ -240,20 +239,18 @@ func (f *File) Cols(sheet string) (*Cols, error) { // visible, err := f.GetColVisible("Sheet1", "D") // func (f *File) GetColVisible(sheet, col string) (bool, error) { - visible := true colNum, err := ColumnNameToNumber(col) if err != nil { - return visible, err + return true, err } - ws, err := f.workSheetReader(sheet) if err != nil { return false, err } if ws.Cols == nil { - return visible, err + return true, err } - + visible := true for c := range ws.Cols.Col { colData := &ws.Cols.Col[c] if colData.Min <= colNum && colNum <= colData.Max { @@ -455,12 +452,12 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { // f := excelize.NewFile() // err := f.SetColWidth("Sheet1", "A", "H", 20) // -func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error { - min, err := ColumnNameToNumber(startcol) +func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error { + min, err := ColumnNameToNumber(startCol) if err != nil { return err } - max, err := ColumnNameToNumber(endcol) + max, err := ColumnNameToNumber(endCol) if err != nil { return err } @@ -502,7 +499,7 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error // flatCols provides a method for the column's operation functions to flatten // and check the worksheet columns. func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) []xlsxCol { - fc := []xlsxCol{} + var fc []xlsxCol for i := col.Min; i <= col.Max; i++ { c := deepcopy.Copy(col).(xlsxCol) c.Min, c.Max = i, i @@ -547,7 +544,7 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) // | | | (x2,y2)| // +-----+------------+------------+ // -// Example of an object that covers some of the area from cell A1 to B2. +// Example of an object that covers some area from cell A1 to B2. // // Based on the width and height of the object we need to calculate 8 vars: // diff --git a/comment.go b/comment.go index 6cebfeea3c..a7c1415ba0 100644 --- a/comment.go +++ b/comment.go @@ -47,7 +47,7 @@ func (f *File) GetComments() (comments map[string][]Comment) { target = "xl" + strings.TrimPrefix(target, "..") } if d := f.commentsReader(strings.TrimPrefix(target, "/")); d != nil { - sheetComments := []Comment{} + var sheetComments []Comment for _, comment := range d.CommentList.Comment { sheetComment := Comment{} if comment.AuthorID < len(d.Authors.Author) { diff --git a/crypt.go b/crypt.go index c579a10b7a..8a783a9a06 100644 --- a/crypt.go +++ b/crypt.go @@ -629,7 +629,7 @@ func genISOPasswdHash(passwd, hashAlgorithm, salt string, spinCount int) (hashVa err = ErrPasswordLengthInvalid return } - hash, ok := map[string]string{ + algorithmName, ok := map[string]string{ "MD4": "md4", "MD5": "md5", "SHA-1": "sha1", @@ -653,11 +653,11 @@ func genISOPasswdHash(passwd, hashAlgorithm, salt string, spinCount int) (hashVa passwordBuffer, _ := encoder.Bytes([]byte(passwd)) b.Write(passwordBuffer) // Generate the initial hash. - key := hashing(hash, b.Bytes()) + key := hashing(algorithmName, b.Bytes()) // Now regenerate until spin count. for i := 0; i < spinCount; i++ { iterator := createUInt32LEBuffer(i, 4) - key = hashing(hash, key, iterator) + key = hashing(algorithmName, key, iterator) } hashValue, saltValue = base64.StdEncoding.EncodeToString(key), base64.StdEncoding.EncodeToString(s) return diff --git a/crypt_test.go b/crypt_test.go index 45e881560c..80f6cc4bcc 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -34,7 +34,7 @@ func TestEncryptionMechanism(t *testing.T) { } func TestHashing(t *testing.T) { - assert.Equal(t, hashing("unsupportedHashAlgorithm", []byte{}), []uint8([]byte(nil))) + assert.Equal(t, hashing("unsupportedHashAlgorithm", []byte{}), []byte(nil)) } func TestGenISOPasswdHash(t *testing.T) { diff --git a/datavalidation.go b/datavalidation.go index d0e927b636..4df2c505ae 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -129,27 +129,27 @@ func (dd *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o D var formula1, formula2 string switch v := f1.(type) { case int: - formula1 = fmt.Sprintf("%d", int(v)) + formula1 = fmt.Sprintf("%d", v) case float64: - if math.Abs(float64(v)) > math.MaxFloat32 { + if math.Abs(v) > math.MaxFloat32 { return ErrDataValidationRange } - formula1 = fmt.Sprintf("%.17g", float64(v)) + formula1 = fmt.Sprintf("%.17g", v) case string: - formula1 = fmt.Sprintf("%s", string(v)) + formula1 = fmt.Sprintf("%s", v) default: return ErrParameterInvalid } switch v := f2.(type) { case int: - formula2 = fmt.Sprintf("%d", int(v)) + formula2 = fmt.Sprintf("%d", v) case float64: - if math.Abs(float64(v)) > math.MaxFloat32 { + if math.Abs(v) > math.MaxFloat32 { return ErrDataValidationRange } - formula2 = fmt.Sprintf("%.17g", float64(v)) + formula2 = fmt.Sprintf("%.17g", v) case string: - formula2 = fmt.Sprintf("%s", string(v)) + formula2 = fmt.Sprintf("%s", v) default: return ErrParameterInvalid } @@ -277,7 +277,7 @@ func (f *File) DeleteDataValidation(sheet, sqref string) error { } dv := ws.DataValidations for i := 0; i < len(dv.DataValidation); i++ { - applySqref := []string{} + var applySqref []string colCells, err := f.flatSqref(dv.DataValidation[i].Sqref) if err != nil { return err @@ -314,7 +314,8 @@ func (f *File) squashSqref(cells [][]int) []string { } else if len(cells) == 0 { return []string{} } - l, r, res := 0, 0, []string{} + var res []string + l, r := 0, 0 for i := 1; i < len(cells); i++ { if cells[i][0] == cells[r][0] && cells[i][1]-cells[r][1] > 1 { curr, _ := f.coordinatesToAreaRef(append(cells[l], cells[r]...)) diff --git a/drawing.go b/drawing.go index e3e7fa87c4..1daa912ae4 100644 --- a/drawing.go +++ b/drawing.go @@ -740,7 +740,7 @@ func (f *File) drawChartShape(formatSet *formatChart) *attrValString { // drawChartSeries provides a function to draw the c:ser element by given // format sets. func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { - ser := []cSer{} + var ser []cSer for k := range formatSet.Series { ser = append(ser, cSer{ IDx: &attrValInt{Val: intPtr(k + formatSet.order)}, diff --git a/errors.go b/errors.go index 1047704ffb..c9d18cb39f 100644 --- a/errors.go +++ b/errors.go @@ -138,9 +138,9 @@ var ( // ErrDefinedNameScope defined the error message on not found defined name // in the given scope. ErrDefinedNameScope = errors.New("no defined name on the scope") - // ErrDefinedNameduplicate defined the error message on the same name + // ErrDefinedNameDuplicate defined the error message on the same name // already exists on the scope. - ErrDefinedNameduplicate = errors.New("the same name already exists on the scope") + ErrDefinedNameDuplicate = errors.New("the same name already exists on the scope") // ErrCustomNumFmt defined the error message on receive the empty custom number format. ErrCustomNumFmt = errors.New("custom number format can not be empty") // ErrFontLength defined the error message on the length of the font diff --git a/excelize.go b/excelize.go index 0aebfc45db..9fe3d8816d 100644 --- a/excelize.go +++ b/excelize.go @@ -102,13 +102,16 @@ func OpenFile(filename string, opt ...Options) (*File, error) { if err != nil { return nil, err } - defer file.Close() f, err := OpenReader(file, opt...) if err != nil { - return nil, err + closeErr := file.Close() + if closeErr == nil { + return f, err + } + return f, closeErr } f.Path = filename - return f, nil + return f, file.Close() } // newFile is object builder diff --git a/excelize_test.go b/excelize_test.go index 7d3830495b..dc5dfccf80 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -40,11 +40,11 @@ func TestOpenFile(t *testing.T) { } assert.NoError(t, f.UpdateLinkedValue()) - assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(100.1588), 'f', -1, 32))) - assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64))) + assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(100.1588, 'f', -1, 32))) + assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(-100.1588, 'f', -1, 64))) // Test set cell value with illegal row number. - assert.EqualError(t, f.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)), + assert.EqualError(t, f.SetCellDefault("Sheet2", "A", strconv.FormatFloat(-100.1588, 'f', -1, 64)), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.SetCellInt("Sheet2", "A1", 100)) @@ -109,7 +109,7 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet2", "F5", int32(1<<32/2-1))) assert.NoError(t, f.SetCellValue("Sheet2", "F6", int64(1<<32/2-1))) assert.NoError(t, f.SetCellValue("Sheet2", "F7", float32(42.65418))) - assert.NoError(t, f.SetCellValue("Sheet2", "F8", float64(-42.65418))) + assert.NoError(t, f.SetCellValue("Sheet2", "F8", -42.65418)) assert.NoError(t, f.SetCellValue("Sheet2", "F9", float32(42))) assert.NoError(t, f.SetCellValue("Sheet2", "F10", float64(42))) assert.NoError(t, f.SetCellValue("Sheet2", "F11", uint(1<<32-1))) @@ -1157,9 +1157,9 @@ func TestHSL(t *testing.T) { assert.Equal(t, 0.0, hueToRGB(0, 0, 2.0/4)) t.Log(RGBToHSL(255, 255, 0)) h, s, l := RGBToHSL(0, 255, 255) - assert.Equal(t, float64(0.5), h) - assert.Equal(t, float64(1), s) - assert.Equal(t, float64(0.5), l) + assert.Equal(t, 0.5, h) + assert.Equal(t, 1.0, s) + assert.Equal(t, 0.5, l) t.Log(RGBToHSL(250, 100, 50)) t.Log(RGBToHSL(50, 100, 250)) t.Log(RGBToHSL(250, 50, 100)) diff --git a/lib.go b/lib.go index 439e50a82a..4205e085ab 100644 --- a/lib.go +++ b/lib.go @@ -80,10 +80,13 @@ func (f *File) unzipToTemp(zipFile *zip.File) (string, error) { if err != nil { return tmp.Name(), err } - _, err = io.Copy(tmp, rc) - rc.Close() - tmp.Close() - return tmp.Name(), err + if _, err = io.Copy(tmp, rc); err != nil { + return tmp.Name(), err + } + if err = rc.Close(); err != nil { + return tmp.Name(), err + } + return tmp.Name(), tmp.Close() } // readXML provides a function to read XML content as bytes. @@ -109,7 +112,7 @@ func (f *File) readBytes(name string) []byte { } content, _ = ioutil.ReadAll(file) f.Pkg.Store(name, content) - file.Close() + _ = file.Close() return content } @@ -437,9 +440,10 @@ func (avb attrValBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error } } start.Attr = []xml.Attr{attr} - e.EncodeToken(start) - e.EncodeToken(start.End()) - return nil + if err := e.EncodeToken(start); err != nil { + return err + } + return e.EncodeToken(start.End()) } // UnmarshalXML convert the literal values true, false, 1, 0 of the XML @@ -558,7 +562,7 @@ func genSheetPasswd(plaintext string) string { charPos++ rotatedBits := value >> 15 // rotated bits beyond bit 15 value &= 0x7fff // first 15 bits - password ^= (value | rotatedBits) + password ^= value | rotatedBits } password ^= int64(len(plaintext)) password ^= 0xCE4B @@ -793,8 +797,8 @@ type Stack struct { // NewStack create a new stack. func NewStack() *Stack { - list := list.New() - return &Stack{list} + l := list.New() + return &Stack{l} } // Push a value onto the top of the stack. diff --git a/merge_test.go b/merge_test.go index a28aeff321..597d4b58a8 100644 --- a/merge_test.go +++ b/merge_test.go @@ -23,7 +23,7 @@ func TestMergeCell(t *testing.T) { assert.NoError(t, f.MergeCell("Sheet1", "G10", "K12")) assert.NoError(t, f.SetCellValue("Sheet1", "G11", "set value in merged cell")) assert.NoError(t, f.SetCellInt("Sheet1", "H11", 100)) - assert.NoError(t, f.SetCellValue("Sheet1", "I11", float64(0.5))) + assert.NoError(t, f.SetCellValue("Sheet1", "I11", 0.5)) assert.NoError(t, f.SetCellHyperLink("Sheet1", "J11", "https://github.com/xuri/excelize", "External")) assert.NoError(t, f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)")) value, err := f.GetCellValue("Sheet1", "H11") diff --git a/numfmt.go b/numfmt.go index 3b20e028d3..685005fbe1 100644 --- a/numfmt.go +++ b/numfmt.go @@ -456,7 +456,7 @@ func localMonthsNameIrish(t time.Time, abbr int) string { } } if abbr == 4 { - return string([]rune(monthNamesIrish[int(t.Month())-1])) + return monthNamesIrish[int(t.Month())-1] } return string([]rune(monthNamesIrish[int(t.Month())-1])[:1]) } @@ -536,7 +536,7 @@ func localMonthsNameRussian(t time.Time, abbr int) string { return string([]rune(month)[:3]) + "." } if abbr == 4 { - return string([]rune(monthNamesRussian[int(t.Month())-1])) + return monthNamesRussian[int(t.Month())-1] } return string([]rune(monthNamesRussian[int(t.Month())-1])[:1]) } @@ -559,7 +559,7 @@ func localMonthsNameThai(t time.Time, abbr int) string { return string(r[:1]) + "." + string(r[len(r)-2:len(r)-1]) + "." } if abbr == 4 { - return string([]rune(monthNamesThai[int(t.Month())-1])) + return monthNamesThai[int(t.Month())-1] } return string([]rune(monthNamesThai[int(t.Month())-1])[:1]) } @@ -575,7 +575,7 @@ func localMonthsNameTibetan(t time.Time, abbr int) string { } return "\u0f5f" } - return string(monthNamesTibetan[int(t.Month())-1]) + return monthNamesTibetan[int(t.Month())-1] } // localMonthsNameTurkish returns the Turkish name of the month. @@ -661,7 +661,7 @@ func localMonthsNameXhosa(t time.Time, abbr int) string { // localMonthsNameYi returns the Yi name of the month. func localMonthsNameYi(t time.Time, abbr int) string { if abbr == 3 || abbr == 4 { - return string([]rune(monthNamesYi[int(t.Month())-1])) + "\ua1aa" + return string(monthNamesYi[int(t.Month())-1]) + "\ua1aa" } return string([]rune(monthNamesYi[int(t.Month())-1])[:1]) } diff --git a/picture.go b/picture.go index 180983d6bc..515f15f942 100644 --- a/picture.go +++ b/picture.go @@ -76,7 +76,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // } // } // -// The optional parameter "autofit" specifies if make image size auto fits the +// The optional parameter "autofit" specifies if you make image size auto-fits the // cell, the default value of that is 'false'. // // The optional parameter "hyperlink" specifies the hyperlink of the image. @@ -86,7 +86,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // cells in this workbook. When the "hyperlink_type" is "Location", // coordinates need to start with "#". // -// The optional parameter "positioning" defines two types of the position of a +// The optional parameter "positioning" defines two types of the position of an // image in an Excel spreadsheet, "oneCell" (Move but don't size with // cells) or "absolute" (Don't move or size with cells). If you don't set this // parameter, the default positioning is move and size with cells. diff --git a/pivotTable.go b/pivotTable.go index 5810968c98..437d22f0e1 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -132,7 +132,7 @@ type PivotTableField struct { // func (f *File) AddPivotTable(opt *PivotTableOption) error { // parameter validation - dataSheet, pivotTableSheetPath, err := f.parseFormatPivotTableSet(opt) + _, pivotTableSheetPath, err := f.parseFormatPivotTableSet(opt) if err != nil { return err } @@ -143,7 +143,7 @@ func (f *File) AddPivotTable(opt *PivotTableOption) error { sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml" pivotTableXML := strings.Replace(sheetRelationshipsPivotTableXML, "..", "xl", -1) pivotCacheXML := "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml" - err = f.addPivotCache(pivotCacheID, pivotCacheXML, opt, dataSheet) + err = f.addPivotCache(pivotCacheXML, opt) if err != nil { return err } @@ -230,7 +230,7 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { // getPivotFieldsOrder provides a function to get order list of pivot table // fields. func (f *File) getPivotFieldsOrder(opt *PivotTableOption) ([]string, error) { - order := []string{} + var order []string dataRange := f.getDefinedNameRefTo(opt.DataRange, opt.pivotTableSheetName) if dataRange == "" { dataRange = opt.DataRange @@ -251,7 +251,7 @@ func (f *File) getPivotFieldsOrder(opt *PivotTableOption) ([]string, error) { } // addPivotCache provides a function to create a pivot cache by given properties. -func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotTableOption, ws *xlsxWorksheet) error { +func (f *File) addPivotCache(pivotCacheXML string, opt *PivotTableOption) error { // validate data range definedNameRef := true dataRange := f.getDefinedNameRefTo(opt.DataRange, opt.pivotTableSheetName) @@ -626,7 +626,7 @@ func (f *File) countPivotCache() int { // getPivotFieldsIndex convert the column of the first row in the data region // to a sequential index by given fields and pivot option. func (f *File) getPivotFieldsIndex(fields []PivotTableField, opt *PivotTableOption) ([]int, error) { - pivotFieldsIndex := []int{} + var pivotFieldsIndex []int orders, err := f.getPivotFieldsOrder(opt) if err != nil { return pivotFieldsIndex, err diff --git a/pivotTable_test.go b/pivotTable_test.go index d7a8eb13b3..2f95ed4986 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -235,15 +235,15 @@ func TestAddPivotTable(t *testing.T) { _, err = f.getPivotFieldsOrder(&PivotTableOption{}) assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) // Test add pivot cache with empty data range - assert.EqualError(t, f.addPivotCache(0, "", &PivotTableOption{}, nil), "parameter 'DataRange' parsing error: parameter is required") + assert.EqualError(t, f.addPivotCache("", &PivotTableOption{}), "parameter 'DataRange' parsing error: parameter is required") // Test add pivot cache with invalid data range - assert.EqualError(t, f.addPivotCache(0, "", &PivotTableOption{ + assert.EqualError(t, f.addPivotCache("", &PivotTableOption{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, - }, nil), "parameter 'DataRange' parsing error: parameter is invalid") + }), "parameter 'DataRange' parsing error: parameter is invalid") // Test add pivot table with empty options assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOption{}), "parameter 'PivotTableRange' parsing error: parameter is required") // Test add pivot table with invalid data range diff --git a/rows.go b/rows.go index ae7e01ed7c..e0918bc60e 100644 --- a/rows.go +++ b/rows.go @@ -179,7 +179,7 @@ type ErrSheetNotExist struct { } func (err ErrSheetNotExist) Error() string { - return fmt.Sprintf("sheet %s is not exist", string(err.SheetName)) + return fmt.Sprintf("sheet %s is not exist", err.SheetName) } // rowXMLIterator defined runtime use field for the worksheet row SAX parser. diff --git a/sheet.go b/sheet.go index 78fcaf2130..3986cd86ad 100644 --- a/sheet.go +++ b/sheet.go @@ -36,7 +36,7 @@ import ( // NewSheet provides the function to create a new sheet by given a worksheet // name and returns the index of the sheets in the workbook // (spreadsheet) after it appended. Note that the worksheet names are not -// case sensitive, when creating a new spreadsheet file, the default +// case-sensitive, when creating a new spreadsheet file, the default // worksheet named `Sheet1` will be created. func (f *File) NewSheet(name string) int { // Check if the worksheet already exists @@ -111,7 +111,7 @@ func (f *File) mergeExpandedCols(ws *xlsxWorksheet) { sort.Slice(ws.Cols.Col, func(i, j int) bool { return ws.Cols.Col[i].Min < ws.Cols.Col[j].Min }) - columns := []xlsxCol{} + var columns []xlsxCol for i, n := 0, len(ws.Cols.Col); i < n; { left := i for i++; i < n && reflect.DeepEqual( @@ -219,10 +219,10 @@ func (f *File) setSheet(index int, name string) { SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, }, } - path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" - f.sheetMap[trimSheetName(name)] = path - f.Sheet.Store(path, &ws) - f.xmlAttr[path] = []xml.Attr{NameSpaceSpreadSheet} + sheetXMLPath := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" + f.sheetMap[trimSheetName(name)] = sheetXMLPath + f.Sheet.Store(sheetXMLPath, &ws) + f.xmlAttr[sheetXMLPath] = []xml.Attr{NameSpaceSpreadSheet} } // relsWriter provides a function to save relationships after @@ -384,7 +384,7 @@ func (f *File) getSheetID(name string) int { } // GetSheetIndex provides a function to get a sheet index of the workbook by -// the given sheet name, the sheet names are not case sensitive. If the given +// the given sheet name, the sheet names are not case-sensitive. If the given // sheet name is invalid or sheet doesn't exist, it will return an integer // type value -1. func (f *File) GetSheetIndex(name string) int { @@ -442,12 +442,12 @@ func (f *File) getSheetMap() map[string]string { for _, v := range f.workbookReader().Sheets.Sheet { for _, rel := range f.relsReader(f.getWorkbookRelsPath()).Relationships { if rel.ID == v.ID { - path := f.getWorksheetPath(rel.Target) - if _, ok := f.Pkg.Load(path); ok { - maps[v.Name] = path + sheetXMLPath := f.getWorksheetPath(rel.Target) + if _, ok := f.Pkg.Load(sheetXMLPath); ok { + maps[v.Name] = sheetXMLPath } - if _, ok := f.tempFiles.Load(path); ok { - maps[v.Name] = path + if _, ok := f.tempFiles.Load(sheetXMLPath); ok { + maps[v.Name] = sheetXMLPath } } } @@ -478,8 +478,8 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } // DeleteSheet provides a function to delete worksheet in a workbook by given -// worksheet name, the sheet names are not case sensitive.the sheet names are -// not case sensitive. Use this method with caution, which will affect +// worksheet name, the sheet names are not case-sensitive. The sheet names are +// not case-sensitive. Use this method with caution, which will affect // changes in references such as formulas, charts, and so on. If there is any // referenced value of the deleted worksheet, it will cause a file error when // you open it. This function will be invalid when only the one worksheet is @@ -601,14 +601,14 @@ func (f *File) copySheet(from, to int) error { } worksheet := deepcopy.Copy(sheet).(*xlsxWorksheet) toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to))) - path := "xl/worksheets/sheet" + toSheetID + ".xml" + sheetXMLPath := "xl/worksheets/sheet" + toSheetID + ".xml" if len(worksheet.SheetViews.SheetView) > 0 { worksheet.SheetViews.SheetView[0].TabSelected = false } worksheet.Drawing = nil worksheet.TableParts = nil worksheet.PageSetUp = nil - f.Sheet.Store(path, worksheet) + f.Sheet.Store(sheetXMLPath, worksheet) toRels := "xl/worksheets/_rels/sheet" + toSheetID + ".xml.rels" fromRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(f.getSheetID(fromSheet)) + ".xml.rels" if rels, ok := f.Pkg.Load(fromRels); ok && rels != nil { @@ -616,7 +616,7 @@ func (f *File) copySheet(from, to int) error { } fromSheetXMLPath := f.sheetMap[trimSheetName(fromSheet)] fromSheetAttr := f.xmlAttr[fromSheetXMLPath] - f.xmlAttr[path] = fromSheetAttr + f.xmlAttr[sheetXMLPath] = fromSheetAttr return err } @@ -779,7 +779,7 @@ func (f *File) SetPanes(sheet, panes string) error { ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = nil } } - s := []*xlsxSelection{} + var s []*xlsxSelection for _, p := range fs.Panes { s = append(s, &xlsxSelection{ ActiveCell: p.ActiveCell, @@ -1207,7 +1207,7 @@ type ( // FitToWidth specified the number of horizontal pages to fit on. FitToWidth int // PageLayoutScale defines the print scaling. This attribute is restricted - // to values ranging from 10 (10%) to 400 (400%). This setting is + // to value ranging from 10 (10%) to 400 (400%). This setting is // overridden when fitToWidth and/or fitToHeight are in use. PageLayoutScale uint ) @@ -1534,7 +1534,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { scope = f.GetSheetName(*dn.LocalSheetID) } if scope == definedName.Scope && dn.Name == definedName.Name { - return ErrDefinedNameduplicate + return ErrDefinedNameDuplicate } } wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) @@ -1616,7 +1616,7 @@ func (f *File) GroupSheets(sheets []string) error { return ErrGroupSheets } // check worksheet exists - wss := []*xlsxWorksheet{} + var wss []*xlsxWorksheet for _, sheet := range sheets { worksheet, err := f.workSheetReader(sheet) if err != nil { diff --git a/sheet_test.go b/sheet_test.go index 429f617247..db36417812 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -276,7 +276,7 @@ func TestDefinedName(t *testing.T) { Name: "Amount", RefersTo: "Sheet1!$A$2:$D$5", Comment: "defined name comment", - }), ErrDefinedNameduplicate.Error()) + }), ErrDefinedNameDuplicate.Error()) assert.EqualError(t, f.DeleteDefinedName(&DefinedName{ Name: "No Exist Defined Name", }), ErrDefinedNameScope.Error()) diff --git a/sheetview.go b/sheetview.go index 8650b322e2..bf8f0237ba 100644 --- a/sheetview.go +++ b/sheetview.go @@ -135,7 +135,7 @@ func (o *View) getSheetViewOption(view *xlsxSheetView) { *o = View(view.View) return } - *o = View("normal") + *o = "normal" } func (o TopLeftCell) setSheetViewOption(view *xlsxSheetView) { @@ -143,7 +143,7 @@ func (o TopLeftCell) setSheetViewOption(view *xlsxSheetView) { } func (o *TopLeftCell) getSheetViewOption(view *xlsxSheetView) { - *o = TopLeftCell(string(view.TopLeftCell)) + *o = TopLeftCell(view.TopLeftCell) } func (o ZoomScale) setSheetViewOption(view *xlsxSheetView) { diff --git a/sparkline.go b/sparkline.go index 5a480b909f..880724a47b 100644 --- a/sparkline.go +++ b/sparkline.go @@ -362,7 +362,7 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // given formatting options. Sparklines are small charts that fit in a single // cell and are used to show trends in data. Sparklines are a feature of Excel // 2010 and later only. You can write them to an XLSX file that can be read by -// Excel 2007 but they won't be displayed. For example, add a grouped +// Excel 2007, but they won't be displayed. For example, add a grouped // sparkline. Changes are applied to all three: // // err := f.AddSparkline("Sheet1", &excelize.SparklineOption{ diff --git a/stream.go b/stream.go index a9ec2cfc8c..c2eda68a40 100644 --- a/stream.go +++ b/stream.go @@ -136,7 +136,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { // Note that the table must be at least two lines including the header. The // header cells must contain strings and must be unique. // -// Currently only one table is allowed for a StreamWriter. AddTable must be +// Currently, only one table is allowed for a StreamWriter. AddTable must be // called after the rows are written but before Flush. // // See File.AddTable for details on the table format. diff --git a/stream_test.go b/stream_test.go index 7a933809e1..3df898a701 100644 --- a/stream_test.go +++ b/stream_test.go @@ -223,7 +223,7 @@ func TestSetCellValFunc(t *testing.T) { assert.NoError(t, sw.setCellValFunc(c, uint32(4294967295))) assert.NoError(t, sw.setCellValFunc(c, uint64(18446744073709551615))) assert.NoError(t, sw.setCellValFunc(c, float32(100.1588))) - assert.NoError(t, sw.setCellValFunc(c, float64(100.1588))) + assert.NoError(t, sw.setCellValFunc(c, 100.1588)) assert.NoError(t, sw.setCellValFunc(c, " Hello")) assert.NoError(t, sw.setCellValFunc(c, []byte(" Hello"))) assert.NoError(t, sw.setCellValFunc(c, time.Now().UTC())) diff --git a/styles.go b/styles.go index 6d6d7bb495..c04ca3b3b3 100644 --- a/styles.go +++ b/styles.go @@ -2465,7 +2465,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { if err != nil { return 0, err } - cellData, col, row, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, axis) if err != nil { return 0, err } @@ -2851,7 +2851,7 @@ func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { if err != nil { return err } - cfRule := []*xlsxCfRule{} + var cfRule []*xlsxCfRule for p, v := range format { var vt, ct string var ok bool @@ -3052,7 +3052,7 @@ func ThemeColor(baseColor string, tint float64) string { h, s, l = RGBToHSL(uint8(r), uint8(g), uint8(b)) } if tint < 0 { - l *= (1 + tint) + l *= 1 + tint } else { l = l*(1-tint) + (1 - (1 - tint)) } diff --git a/styles_test.go b/styles_test.go index de3444fb07..a71041dd1e 100644 --- a/styles_test.go +++ b/styles_test.go @@ -212,10 +212,10 @@ func TestNewStyle(t *testing.T) { assert.EqualError(t, err, ErrFontSize.Error()) // new numeric custom style - fmt := "####;####" + numFmt := "####;####" f.Styles.NumFmts = nil styleID, err = f.NewStyle(&Style{ - CustomNumFmt: &fmt, + CustomNumFmt: &numFmt, }) assert.NoError(t, err) assert.Equal(t, 2, styleID) diff --git a/table.go b/table.go index 1fcb448604..0311a8ed11 100644 --- a/table.go +++ b/table.go @@ -383,7 +383,7 @@ func (f *File) writeAutoFilter(filter *xlsxAutoFilter, exp []int, tokens []strin filter.FilterColumn[0].Filters = &xlsxFilters{Filter: filters} } else if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 { // Double equality with "or" operator. - filters := []*xlsxFilter{} + var filters []*xlsxFilter for _, v := range tokens { filters = append(filters, &xlsxFilter{Val: v}) } @@ -419,7 +419,7 @@ func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val strin if filter.FilterColumn[0].CustomFilters != nil { filter.FilterColumn[0].CustomFilters.CustomFilter = append(filter.FilterColumn[0].CustomFilters.CustomFilter, &customFilter) } else { - customFilters := []*xlsxCustomFilter{} + var customFilters []*xlsxCustomFilter customFilters = append(customFilters, &customFilter) filter.FilterColumn[0].CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters} } @@ -435,8 +435,8 @@ func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val strin // ('x', '>', 2000, 'and', 'x', '<', 5000) -> exp1 and exp2 // func (f *File) parseFilterExpression(expression string, tokens []string) ([]int, []string, error) { - expressions := []int{} - t := []string{} + var expressions []int + var t []string if len(tokens) == 7 { // The number of tokens will be either 3 (for 1 expression) or 7 (for 2 // expressions). diff --git a/templates.go b/templates.go index 94683417a5..1e46b56175 100644 --- a/templates.go +++ b/templates.go @@ -14,12 +14,6 @@ package excelize -import "encoding/xml" - -// XMLHeaderByte define an XML declaration can also contain a standalone -// declaration. -var XMLHeaderByte = []byte(xml.Header) - const ( defaultXMLPathContentTypes = "[Content_Types].xml" defaultXMLPathDocPropsApp = "docProps/app.xml" diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 401bf2c894..f578033953 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -66,13 +66,13 @@ type xlsxCalcChain struct { // | same dependency level. Child chains are series of // | calculations that can be independently farmed out to // | other threads or processors.The possible values for -// | this attribute are defined by the W3C XML Schema +// | this attribute is defined by the W3C XML Schema // | boolean datatype. // | // t (New Thread) | A Boolean flag indicating whether the cell's formula // | starts a new thread. True if the cell's formula starts // | a new thread, false otherwise.The possible values for -// | this attribute are defined by the W3C XML Schema +// | this attribute is defined by the W3C XML Schema // | boolean datatype. // type xlsxCalcChainC struct { diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 5920f1f36f..4b3cd64275 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -16,7 +16,7 @@ import ( "sync" ) -// xlsxTypes directly maps the types element of content types for relationship +// xlsxTypes directly maps the types' element of content types for relationship // parts, it takes a Multipurpose Internet Mail Extension (MIME) media type as a // value. type xlsxTypes struct { diff --git a/xmlStyles.go b/xmlStyles.go index c70ab605fd..71fe9a66f9 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -197,7 +197,7 @@ type xlsxCellStyle struct { // contains the master formatting records (xf's) which define the formatting for // all named cell styles in this workbook. Master formatting records reference // individual elements of formatting (e.g., number format, font definitions, -// cell fills, etc) by specifying a zero-based index into those collections. +// cell fills, etc.) by specifying a zero-based index into those collections. // Master formatting records also specify whether to apply or ignore particular // aspects of formatting. type xlsxCellStyleXfs struct { diff --git a/xmlTable.go b/xmlTable.go index 4afc26d818..5a56a8330d 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -14,7 +14,7 @@ package excelize import "encoding/xml" // xlsxTable directly maps the table element. A table helps organize and provide -// structure to lists of information in a worksheet. Tables have clearly labeled +// structure to list of information in a worksheet. Tables have clearly labeled // columns, rows, and data regions. Tables make it easier for users to sort, // analyze, format, manage, add, and delete information. This element is the // root element for a table that is not a single cell XML table. From 4affeacc45166ba1b1edfe3036249fd6426dc76c Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 25 Mar 2022 00:41:48 +0800 Subject: [PATCH 012/213] ref #65, new formula functions: F.DIST and FDIST --- calc.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++-- calc_test.go | 26 ++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index f707ee5c3e..c2fc15cad9 100644 --- a/calc.go +++ b/calc.go @@ -69,7 +69,7 @@ const ( searchModeDescBinary = -2 maxFinancialIterations = 128 - financialPercision = 1.0e-08 + financialPrecision = 1.0e-08 // Date and time format regular expressions monthRe = `((jan|january)|(feb|february)|(mar|march)|(apr|april)|(may)|(jun|june)|(jul|july)|(aug|august)|(sep|september)|(oct|october)|(nov|november)|(dec|december))` df1 = `(([0-9])+)/(([0-9])+)/(([0-9])+)` @@ -424,6 +424,8 @@ type formulaFuncs struct { // FACT // FACTDOUBLE // FALSE +// F.DIST +// FDIST // FIND // FINDB // F.INV @@ -7056,6 +7058,85 @@ func (fn *formulaFuncs) EXPONDIST(argsList *list.List) formulaArg { return newNumberFormulaArg(lambda.Number * math.Exp(-lambda.Number*x.Number)) } +// FdotDIST function calculates the Probability Density Function or the +// Cumulative Distribution Function for the F Distribution. This function is +// frequently used used to measure the degree of diversity between two data +// sets. The syntax of the function is: +// +// F.DIST(x,deg_freedom1,deg_freedom2,cumulative) +// +func (fn *formulaFuncs) FdotDIST(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "F.DIST requires 4 arguments") + } + var x, deg1, deg2, cumulative formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if deg1 = argsList.Front().Next().Value.(formulaArg).ToNumber(); deg1.Type != ArgNumber { + return deg1 + } + if deg2 = argsList.Front().Next().Next().Value.(formulaArg).ToNumber(); deg2.Type != ArgNumber { + return deg2 + } + if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError { + return cumulative + } + if x.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + maxDeg := math.Pow10(10) + if deg1.Number < 1 || deg1.Number >= maxDeg { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if deg2.Number < 1 || deg2.Number >= maxDeg { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if cumulative.Number == 1 { + return newNumberFormulaArg(1 - getBetaDist(deg2.Number/(deg2.Number+deg1.Number*x.Number), deg2.Number/2, deg1.Number/2)) + } + return newNumberFormulaArg(math.Gamma((deg2.Number+deg1.Number)/2) / (math.Gamma(deg1.Number/2) * math.Gamma(deg2.Number/2)) * math.Pow(deg1.Number/deg2.Number, deg1.Number/2) * (math.Pow(x.Number, (deg1.Number-2)/2) / math.Pow(1+(deg1.Number/deg2.Number)*x.Number, (deg1.Number+deg2.Number)/2))) +} + +// FDIST function calculates the (right-tailed) F Probability Distribution, +// which measures the degree of diversity between two data sets. The syntax +// of the function is: +// +// FDIST(x,deg_freedom1,deg_freedom2) +// +func (fn *formulaFuncs) FDIST(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "FDIST requires 3 arguments") + } + var x, deg1, deg2 formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if deg1 = argsList.Front().Next().Value.(formulaArg).ToNumber(); deg1.Type != ArgNumber { + return deg1 + } + if deg2 = argsList.Back().Value.(formulaArg).ToNumber(); deg2.Type != ArgNumber { + return deg2 + } + if x.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + maxDeg := math.Pow10(10) + if deg1.Number < 1 || deg1.Number >= maxDeg { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if deg2.Number < 1 || deg2.Number >= maxDeg { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + args := list.New() + args.PushBack(newNumberFormulaArg(deg1.Number * x.Number / (deg1.Number*x.Number + deg2.Number))) + args.PushBack(newNumberFormulaArg(0.5 * deg1.Number)) + args.PushBack(newNumberFormulaArg(0.5 * deg2.Number)) + args.PushBack(newNumberFormulaArg(0)) + args.PushBack(newNumberFormulaArg(1)) + return newNumberFormulaArg(1 - fn.BETADIST(args).Number) +} + // prepareFinvArgs checking and prepare arguments for the formula function // F.INV, F.INV.RT and FINV. func (fn *formulaFuncs) prepareFinvArgs(name string, argsList *list.List) formulaArg { @@ -12982,7 +13063,7 @@ func (fn *formulaFuncs) IRR(argsList *list.List) formulaArg { if fMid <= 0 { rtb = xMid } - if math.Abs(fMid) < financialPercision || math.Abs(dx) < financialPercision { + if math.Abs(fMid) < financialPrecision || math.Abs(dx) < financialPrecision { break } } diff --git a/calc_test.go b/calc_test.go index 6708cdbbbb..1b59e78bc1 100644 --- a/calc_test.go +++ b/calc_test.go @@ -935,6 +935,11 @@ func TestCalcCellValue(t *testing.T) { "=EXPONDIST(0.5,1,TRUE)": "0.393469340287367", "=EXPONDIST(0.5,1,FALSE)": "0.606530659712633", "=EXPONDIST(2,1,TRUE)": "0.864664716763387", + // FDIST + "=FDIST(5,1,2)": "0.154845745271483", + // F.DIST + "=F.DIST(1,2,5,TRUE)": "0.568798849628308", + "=F.DIST(1,2,5,FALSE)": "0.308000821694066", // F.INV "=F.INV(0.9,2,5)": "3.77971607877395", // FINV @@ -2631,6 +2636,27 @@ func TestCalcCellValue(t *testing.T) { "=EXPONDIST(0,1,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", "=EXPONDIST(-1,1,TRUE)": "#NUM!", "=EXPONDIST(1,0,TRUE)": "#NUM!", + // FDIST + "=FDIST()": "FDIST requires 3 arguments", + "=FDIST(\"\",1,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=FDIST(5,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=FDIST(5,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=FDIST(-1,1,2)": "#NUM!", + "=FDIST(5,0,2)": "#NUM!", + "=FDIST(5,10000000000,2)": "#NUM!", + "=FDIST(5,1,0)": "#NUM!", + "=FDIST(5,1,10000000000)": "#NUM!", + // F.DIST + "=F.DIST()": "F.DIST requires 4 arguments", + "=F.DIST(\"\",2,5,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=F.DIST(1,\"\",5,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=F.DIST(1,2,\"\",TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=F.DIST(1,2,5,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=F.DIST(-1,1,2,TRUE)": "#NUM!", + "=F.DIST(5,0,2,TRUE)": "#NUM!", + "=F.DIST(5,10000000000,2,TRUE)": "#NUM!", + "=F.DIST(5,1,0,TRUE)": "#NUM!", + "=F.DIST(5,1,10000000000,TRUE)": "#NUM!", // F.INV "=F.INV()": "F.INV requires 3 arguments", "=F.INV(\"\",1,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", From 17141a963878adc29e1fcc210c488e1ae3fe9da3 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 26 Mar 2022 11:15:04 +0800 Subject: [PATCH 013/213] ref #65, new formula functions: F.DIST.RT and SERIESSUM --- calc.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- calc_test.go | 21 +++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/calc.go b/calc.go index c2fc15cad9..c277212b86 100644 --- a/calc.go +++ b/calc.go @@ -425,6 +425,7 @@ type formulaFuncs struct { // FACTDOUBLE // FALSE // F.DIST +// F.DIST.RT // FDIST // FIND // FINDB @@ -609,6 +610,7 @@ type formulaFuncs struct { // SEC // SECH // SECOND +// SERIESSUM // SHEET // SHEETS // SIGN @@ -4655,6 +4657,40 @@ func (fn *formulaFuncs) SECH(argsList *list.List) formulaArg { return newNumberFormulaArg(1 / math.Cosh(number.Number)) } +// SERIESSUM function returns the sum of a power series. The syntax of the +// function is: +// +// SERIESSUM(x,n,m,coefficients) +// +func (fn *formulaFuncs) SERIESSUM(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "SERIESSUM requires 4 arguments") + } + var x, n, m formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if n = argsList.Front().Next().Value.(formulaArg).ToNumber(); n.Type != ArgNumber { + return n + } + if m = argsList.Front().Next().Next().Value.(formulaArg).ToNumber(); m.Type != ArgNumber { + return m + } + var result, i float64 + for _, coefficient := range argsList.Back().Value.(formulaArg).ToList() { + if coefficient.Value() == "" { + continue + } + num := coefficient.ToNumber() + if num.Type != ArgNumber { + return num + } + result += num.Number * math.Pow(x.Number, (n.Number+(m.Number*float64(i)))) + i++ + } + return newNumberFormulaArg(result) +} + // SIGN function returns the arithmetic sign (+1, -1 or 0) of a supplied // number. I.e. if the number is positive, the Sign function returns +1, if // the number is negative, the function returns -1 and if the number is 0 @@ -7137,6 +7173,19 @@ func (fn *formulaFuncs) FDIST(argsList *list.List) formulaArg { return newNumberFormulaArg(1 - fn.BETADIST(args).Number) } +// FdotDISTdotRT function calculates the (right-tailed) F Probability +// Distribution, which measures the degree of diversity between two data sets. +// The syntax of the function is: +// +// F.DIST.RT(x,deg_freedom1,deg_freedom2) +// +func (fn *formulaFuncs) FdotDISTdotRT(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "F.DIST.RT requires 3 arguments") + } + return fn.FDIST(argsList) +} + // prepareFinvArgs checking and prepare arguments for the formula function // F.INV, F.INV.RT and FINV. func (fn *formulaFuncs) prepareFinvArgs(name string, argsList *list.List) formulaArg { @@ -11431,7 +11480,7 @@ func lookupBinarySearch(vertical bool, lookupValue, lookupArray, matchMode, sear } else { tableArray = lookupArray.Matrix[0] } - var low, high, lastMatchIdx = 0, len(tableArray) - 1, -1 + low, high, lastMatchIdx := 0, len(tableArray)-1, -1 count := high for low <= high { mid := low + (high-low)/2 diff --git a/calc_test.go b/calc_test.go index 1b59e78bc1..f9da26dfb4 100644 --- a/calc_test.go +++ b/calc_test.go @@ -668,6 +668,9 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.SECH(-3.14159265358979)": "0.0862667383340547", "=_xlfn.SECH(0)": "1", "=_xlfn.SECH(_xlfn.SECH(0))": "0.648054273663885", + // SERIESSUM + "=SERIESSUM(1,2,3,A1:A4)": "6", + "=SERIESSUM(1,2,3,A1:B5)": "15", // SIGN "=SIGN(9.5)": "1", "=SIGN(-9.5)": "-1", @@ -940,6 +943,8 @@ func TestCalcCellValue(t *testing.T) { // F.DIST "=F.DIST(1,2,5,TRUE)": "0.568798849628308", "=F.DIST(1,2,5,FALSE)": "0.308000821694066", + // F.DIST.RT + "=F.DIST.RT(5,1,2)": "0.154845745271483", // F.INV "=F.INV(0.9,2,5)": "3.77971607877395", // FINV @@ -2315,6 +2320,12 @@ func TestCalcCellValue(t *testing.T) { // _xlfn.SECH "=_xlfn.SECH()": "SECH requires 1 numeric argument", `=_xlfn.SECH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + // SERIESSUM + "=SERIESSUM()": "SERIESSUM requires 4 arguments", + "=SERIESSUM(\"\",2,3,A1:A4)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=SERIESSUM(1,\"\",3,A1:A4)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=SERIESSUM(1,2,\"\",A1:A4)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=SERIESSUM(1,2,3,A1:D1)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", // SIGN "=SIGN()": "SIGN requires 1 numeric argument", `=SIGN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -2657,6 +2668,16 @@ func TestCalcCellValue(t *testing.T) { "=F.DIST(5,10000000000,2,TRUE)": "#NUM!", "=F.DIST(5,1,0,TRUE)": "#NUM!", "=F.DIST(5,1,10000000000,TRUE)": "#NUM!", + // F.DIST.RT + "=F.DIST.RT()": "F.DIST.RT requires 3 arguments", + "=F.DIST.RT(\"\",1,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=F.DIST.RT(5,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=F.DIST.RT(5,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=F.DIST.RT(-1,1,2)": "#NUM!", + "=F.DIST.RT(5,0,2)": "#NUM!", + "=F.DIST.RT(5,10000000000,2)": "#NUM!", + "=F.DIST.RT(5,1,0)": "#NUM!", + "=F.DIST.RT(5,1,10000000000)": "#NUM!", // F.INV "=F.INV()": "F.INV requires 3 arguments", "=F.INV(\"\",1,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", From f8d763d0bd6d9e288d68d2b048023bcbefb63bce Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 27 Mar 2022 11:53:45 +0800 Subject: [PATCH 014/213] ref #65, new formula functions: CHITEST and CHISQ.TEST --- calc.go | 121 +++++++++++++++++++++++++++++++++++++++++++++++---- calc_test.go | 50 ++++++++++++++++++++- crypt.go | 2 +- 3 files changed, 163 insertions(+), 10 deletions(-) diff --git a/calc.go b/calc.go index c277212b86..a87fa2f587 100644 --- a/calc.go +++ b/calc.go @@ -356,6 +356,8 @@ type formulaFuncs struct { // CHAR // CHIDIST // CHIINV +// CHITEST +// CHISQ.TEST // CHOOSE // CLEAN // CODE @@ -1243,7 +1245,7 @@ func isOperatorPrefixToken(token efp.Token) bool { return (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || (ok && token.TType == efp.TokenTypeOperatorInfix) } -// isOperand determine if the token is parse operand perand. +// isOperand determine if the token is parse operand. func isOperand(token efp.Token) bool { return token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText) } @@ -4685,7 +4687,7 @@ func (fn *formulaFuncs) SERIESSUM(argsList *list.List) formulaArg { if num.Type != ArgNumber { return num } - result += num.Number * math.Pow(x.Number, (n.Number+(m.Number*float64(i)))) + result += num.Number * math.Pow(x.Number, n.Number+(m.Number*i)) i++ } return newNumberFormulaArg(result) @@ -6224,7 +6226,7 @@ func (fn *formulaFuncs) BINOMdotDISTdotRANGE(argsList *list.List) formulaArg { return newNumberFormulaArg(sum) } -// binominv implement inverse of the binomial distribution calcuation. +// binominv implement inverse of the binomial distribution calculation. func binominv(n, p, alpha float64) float64 { q, i, sum, max := 1-p, 0.0, 0.0, 0.0 n = math.Floor(n) @@ -6292,11 +6294,55 @@ func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { if x.Type != ArgNumber { return x } - degress := argsList.Back().Value.(formulaArg).ToNumber() - if degress.Type != ArgNumber { - return degress + degrees := argsList.Back().Value.(formulaArg).ToNumber() + if degrees.Type != ArgNumber { + return degrees } - return newNumberFormulaArg(1 - (incompleteGamma(degress.Number/2, x.Number/2) / math.Gamma(degress.Number/2))) + logSqrtPi, sqrtPi := math.Log(math.Sqrt(math.Pi)), 1/math.Sqrt(math.Pi) + var e, s, z, c, y float64 + a, x1, even := x.Number/2, x.Number, int(degrees.Number)%2 == 0 + if degrees.Number > 1 { + y = math.Exp(-a) + } + args := list.New() + args.PushBack(newNumberFormulaArg(-math.Sqrt(x1))) + o := fn.NORMSDIST(args) + s = 2 * o.Number + if even { + s = y + } + if degrees.Number > 2 { + x1 = (degrees.Number - 1) / 2 + z = 0.5 + if even { + z = 1 + } + if a > 20 { + e = logSqrtPi + if even { + e = 0 + } + c = math.Log(a) + for z <= x1 { + e = math.Log(z) + e + s += math.Exp(c*z - a - e) + z += 1 + } + return newNumberFormulaArg(s) + } + e = sqrtPi / math.Sqrt(a) + if even { + e = 1 + } + c = 0 + for z <= x1 { + e = e * (a / z) + c = c + e + z += 1 + } + return newNumberFormulaArg(c*y + s) + } + return newNumberFormulaArg(s) } // CHIINV function calculates the inverse of the right-tailed probability of @@ -6325,6 +6371,65 @@ func (fn *formulaFuncs) CHIINV(argsList *list.List) formulaArg { return newNumberFormulaArg(gammainv(1-probability.Number, 0.5*deg.Number, 2.0)) } +// CHITEST function uses the chi-square test to calculate the probability that +// the differences between two supplied data sets (of observed and expected +// frequencies), are likely to be simply due to sampling error, or if they are +// likely to be real. The syntax of the function is: +// +// CHITEST(actual_range,expected_range) +// +func (fn *formulaFuncs) CHITEST(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "CHITEST requires 2 arguments") + } + actual, expected := argsList.Front().Value.(formulaArg), argsList.Back().Value.(formulaArg) + actualList, expectedList := actual.ToList(), expected.ToList() + rows := len(actual.Matrix) + columns := len(actualList) / rows + if len(actualList) != len(expectedList) || len(actualList) == 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + var result float64 + var degrees int + for i := 0; i < len(actualList); i++ { + a, e := actualList[i].ToNumber(), expectedList[i].ToNumber() + if a.Type == ArgNumber && e.Type == ArgNumber { + if e.Number == 0 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + if e.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + result += (a.Number - e.Number) * (a.Number - e.Number) / e.Number + } + } + if rows == 1 { + degrees = columns - 1 + } else if columns == 1 { + degrees = rows - 1 + } else { + degrees = (columns - 1) * (rows - 1) + } + args := list.New() + args.PushBack(newNumberFormulaArg(result)) + args.PushBack(newNumberFormulaArg(float64(degrees))) + return fn.CHIDIST(args) +} + +// CHISQdotTEST function performs the chi-square test on two supplied data sets +// (of observed and expected frequencies), and returns the probability that +// the differences between the sets are simply due to sampling error. The +// syntax of the function is: +// +// CHISQ.TEST(actual_range,expected_range) +// +func (fn *formulaFuncs) CHISQdotTEST(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.TEST requires 2 arguments") + } + return fn.CHITEST(argsList) +} + // confidence is an implementation of the formula functions CONFIDENCE and // CONFIDENCE.NORM. func (fn *formulaFuncs) confidence(name string, argsList *list.List) formulaArg { @@ -7096,7 +7201,7 @@ func (fn *formulaFuncs) EXPONDIST(argsList *list.List) formulaArg { // FdotDIST function calculates the Probability Density Function or the // Cumulative Distribution Function for the F Distribution. This function is -// frequently used used to measure the degree of diversity between two data +// frequently used to measure the degree of diversity between two data // sets. The syntax of the function is: // // F.DIST(x,deg_freedom1,deg_freedom2,cumulative) diff --git a/calc_test.go b/calc_test.go index f9da26dfb4..308db55d9a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -843,7 +843,9 @@ func TestCalcCellValue(t *testing.T) { "=BINOM.INV(100,0.5,90%)": "56", // CHIDIST "=CHIDIST(0.5,3)": "0.918891411654676", - "=CHIDIST(8,3)": "0.0460117056892315", + "=CHIDIST(8,3)": "0.0460117056892314", + "=CHIDIST(40,4)": "4.32842260712097E-08", + "=CHIDIST(42,4)": "1.66816329414062E-08", // CHIINV "=CHIINV(0.5,1)": "0.454936423119572", "=CHIINV(0.75,1)": "0.101531044267622", @@ -4213,6 +4215,52 @@ func TestCalcHLOOKUP(t *testing.T) { } } +func TestCalcCHITESTandCHISQdotTEST(t *testing.T) { + cellData := [][]interface{}{ + {nil, "Observed Frequencies", nil, nil, "Expected Frequencies"}, + {nil, "men", "women", nil, nil, "men", "women"}, + {"answer a", 33, 39, nil, "answer a", 26.25, 31.5}, + {"answer b", 62, 62, nil, "answer b", 57.75, 61.95}, + {"answer c", 10, 4, nil, "answer c", 21, 11.55}, + {nil, -1, 0}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=CHITEST(B3:C5,F3:G5)": "0.000699102758787672", + "=CHITEST(B3:C3,F3:G3)": "0.0605802098655177", + "=CHITEST(B3:B4,F3:F4)": "0.152357748933542", + "=CHITEST(B4:B6,F3:F5)": "7.07076951440726E-25", + "=CHISQ.TEST(B3:C5,F3:G5)": "0.000699102758787672", + "=CHISQ.TEST(B3:C3,F3:G3)": "0.0605802098655177", + "=CHISQ.TEST(B3:B4,F3:F4)": "0.152357748933542", + "=CHISQ.TEST(B4:B6,F3:F5)": "7.07076951440726E-25", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "I1", formula)) + result, err := f.CalcCellValue("Sheet1", "I1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError := map[string]string{ + "=CHITEST()": "CHITEST requires 2 arguments", + "=CHITEST(B3:C5,F3:F4)": "#N/A", + "=CHITEST(B3:B3,F3:F3)": "#N/A", + "=CHITEST(F3:F5,B4:B6)": "#NUM!", + "=CHITEST(F3:F5,C4:C6)": "#DIV/0!", + "=CHISQ.TEST()": "CHISQ.TEST requires 2 arguments", + "=CHISQ.TEST(B3:C5,F3:F4)": "#N/A", + "=CHISQ.TEST(B3:B3,F3:F3)": "#N/A", + "=CHISQ.TEST(F3:F5,B4:B6)": "#NUM!", + "=CHISQ.TEST(F3:F5,C4:C6)": "#DIV/0!", + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "I1", formula)) + result, err := f.CalcCellValue("Sheet1", "I1") + assert.EqualError(t, err, expected, formula) + assert.Equal(t, "", result, formula) + } +} + func TestCalcIRR(t *testing.T) { cellData := [][]interface{}{{-1}, {0.2}, {0.24}, {0.288}, {0.3456}, {0.4147}} f := prepareCalcData(cellData) diff --git a/crypt.go b/crypt.go index 8a783a9a06..da9feb4a65 100644 --- a/crypt.go +++ b/crypt.go @@ -128,7 +128,7 @@ type StandardEncryptionVerifier struct { EncryptedVerifierHash []byte } -// Decrypt API decrypt the CFB file format with ECMA-376 agile encryption and +// Decrypt API decrypts the CFB file format with ECMA-376 agile encryption and // standard encryption. Support cryptographic algorithm: MD4, MD5, RIPEMD-160, // SHA1, SHA256, SHA384 and SHA512 currently. func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { From 46336bc788ce344533524a29bc9858d183f91aeb Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 28 Mar 2022 08:13:47 +0800 Subject: [PATCH 015/213] ref #65, new formula functions: CHISQ.DIST.RT CHISQ.DIST and GAMMALN.PRECISE --- calc.go | 216 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 31 ++++++++ 2 files changed, 247 insertions(+) diff --git a/calc.go b/calc.go index a87fa2f587..84f8568245 100644 --- a/calc.go +++ b/calc.go @@ -357,6 +357,8 @@ type formulaFuncs struct { // CHIDIST // CHIINV // CHITEST +// CHISQ.DIST +// CHISQ.DIST.RT // CHISQ.TEST // CHOOSE // CLEAN @@ -449,6 +451,7 @@ type formulaFuncs struct { // GAMMA.INV // GAMMAINV // GAMMALN +// GAMMALN.PRECISE // GAUSS // GCD // GEOMEAN @@ -6416,6 +6419,200 @@ func (fn *formulaFuncs) CHITEST(argsList *list.List) formulaArg { return fn.CHIDIST(args) } +// getGammaSeries calculates a power-series of the gamma function. +func getGammaSeries(fA, fX float64) float64 { + var ( + fHalfMachEps = 2.22045e-016 / 2 + fDenomfactor = fA + fSummand = 1 / fA + fSum = fSummand + nCount = 1 + ) + for fSummand/fSum > fHalfMachEps && nCount <= 10000 { + fDenomfactor = fDenomfactor + 1 + fSummand = fSummand * fX / fDenomfactor + fSum = fSum + fSummand + nCount = nCount + 1 + } + return fSum +} + +// getGammaContFraction returns continued fraction with odd items of the gamma +// function. +func getGammaContFraction(fA, fX float64) float64 { + var ( + fBigInv = 2.22045e-016 + fHalfMachEps = fBigInv / 2 + fBig = 1 / fBigInv + fCount = 0.0 + fY = 1 - fA + fDenom = fX + 2 - fA + fPkm1 = fX + 1 + fPkm2 = 1.0 + fQkm1 = fDenom * fX + fQkm2 = fX + fApprox = fPkm1 / fQkm1 + bFinished = false + ) + for !bFinished && fCount < 10000 { + fCount = fCount + 1 + fY = fY + 1 + fDenom = fDenom + 2 + var ( + fNum = fY * fCount + f1 = fPkm1 * fDenom + f2 = fPkm2 * fNum + fPk = math.Nextafter(f1, f1) - math.Nextafter(f2, f2) + f3 = fQkm1 * fDenom + f4 = fQkm2 * fNum + fQk = math.Nextafter(f3, f3) - math.Nextafter(f4, f4) + ) + if fQk != 0 { + var fR = fPk / fQk + bFinished = math.Abs((fApprox-fR)/fR) <= fHalfMachEps + fApprox = fR + } + fPkm2, fPkm1, fQkm2, fQkm1 = fPkm1, fPk, fQkm1, fQk + if math.Abs(fPk) > fBig { + // reduce a fraction does not change the value + fPkm2 = fPkm2 * fBigInv + fPkm1 = fPkm1 * fBigInv + fQkm2 = fQkm2 * fBigInv + fQkm1 = fQkm1 * fBigInv + } + } + return fApprox +} + +// getLogGammaHelper is a part of implementation of the function getLogGamma. +func getLogGammaHelper(fZ float64) float64 { + var _fg = 6.024680040776729583740234375 + var zgHelp = fZ + _fg - 0.5 + return math.Log(getLanczosSum(fZ)) + (fZ-0.5)*math.Log(zgHelp) - zgHelp +} + +// getGammaHelper is a part of implementation of the function getLogGamma. +func getGammaHelper(fZ float64) float64 { + var ( + gamma = getLanczosSum(fZ) + fg = 6.024680040776729583740234375 + zgHelp = fZ + fg - 0.5 + // avoid intermediate overflow + halfpower = math.Pow(zgHelp, fZ/2-0.25) + ) + gamma *= halfpower + gamma /= math.Exp(zgHelp) + gamma *= halfpower + if fZ <= 20 && fZ == math.Floor(fZ) { + gamma = math.Round(gamma) + } + return gamma +} + +// getLogGamma calculates the natural logarithm of the gamma function. +func getLogGamma(fZ float64) float64 { + var fMaxGammaArgument = 171.624376956302 + if fZ >= fMaxGammaArgument { + return getLogGammaHelper(fZ) + } + if fZ >= 1.0 { + return math.Log(getGammaHelper(fZ)) + } + if fZ >= 0.5 { + return math.Log(getGammaHelper(fZ+1) / fZ) + } + return getLogGammaHelper(fZ+2) - math.Log(fZ+1) - math.Log(fZ) +} + +// getLowRegIGamma returns lower regularized incomplete gamma function. +func getLowRegIGamma(fA, fX float64) float64 { + fLnFactor := fA*math.Log(fX) - fX - getLogGamma(fA) + fFactor := math.Exp(fLnFactor) + if fX > fA+1 { + return 1 - fFactor*getGammaContFraction(fA, fX) + } + return fFactor * getGammaSeries(fA, fX) +} + +// getChiSqDistCDF returns left tail for the Chi-Square distribution. +func getChiSqDistCDF(fX, fDF float64) float64 { + if fX <= 0 { + return 0 + } + return getLowRegIGamma(fDF/2, fX/2) +} + +// getChiSqDistPDF calculates the probability density function for the +// Chi-Square distribution. +func getChiSqDistPDF(fX, fDF float64) float64 { + if fDF*fX > 1391000 { + return math.Exp((0.5*fDF-1)*math.Log(fX*0.5) - 0.5*fX - math.Log(2) - getLogGamma(0.5*fDF)) + } + var fCount, fValue float64 + if math.Mod(fDF, 2) < 0.5 { + fValue = 0.5 + fCount = 2 + } else { + fValue = 1 / math.Sqrt(fX*2*math.Pi) + fCount = 1 + } + for fCount < fDF { + fValue *= fX / fCount + fCount += 2 + } + if fX >= 1425 { + fValue = math.Exp(math.Log(fValue) - fX/2) + } else { + fValue *= math.Exp(-fX / 2) + } + return fValue +} + +// CHISQdotDIST function calculates the Probability Density Function or the +// Cumulative Distribution Function for the Chi-Square Distribution. The +// syntax of the function is: +// +// CHISQ.DIST(x,degrees_freedom,cumulative) +// +func (fn *formulaFuncs) CHISQdotDIST(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.DIST requires 3 arguments") + } + var x, degrees, cumulative formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if degrees = argsList.Front().Next().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber { + return degrees + } + if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError { + return cumulative + } + if x.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + maxDeg := math.Pow10(10) + if degrees.Number < 1 || degrees.Number >= maxDeg { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if cumulative.Number == 1 { + return newNumberFormulaArg(getChiSqDistCDF(x.Number, degrees.Number)) + } + return newNumberFormulaArg(getChiSqDistPDF(x.Number, degrees.Number)) +} + +// CHISQdotDISTdotRT function calculates the right-tailed probability of the +// Chi-Square Distribution. The syntax of the function is: +// +// CHISQ.DIST.RT(x,degrees_freedom) +// +func (fn *formulaFuncs) CHISQdotDISTdotRT(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.DIST.RT requires 2 numeric arguments") + } + return fn.CHIDIST(argsList) +} + // CHISQdotTEST function performs the chi-square test on two supplied data sets // (of observed and expected frequencies), and returns the probability that // the differences between the sets are simply due to sampling error. The @@ -7033,6 +7230,25 @@ func (fn *formulaFuncs) GAMMALN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") } +// GAMMALNdotPRECISE function returns the natural logarithm of the Gamma +// Function, Γ(n). The syntax of the function is: +// +// GAMMALN.PRECISE(x) +// +func (fn *formulaFuncs) GAMMALNdotPRECISE(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN.PRECISE requires 1 numeric argument") + } + x := argsList.Front().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + if x.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(getLogGamma(x.Number)) +} + // GAUSS function returns the probability that a member of a standard normal // population will fall between the mean and a specified number of standard // deviations from the mean. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 308db55d9a..306e24d99c 100644 --- a/calc_test.go +++ b/calc_test.go @@ -851,6 +851,20 @@ func TestCalcCellValue(t *testing.T) { "=CHIINV(0.75,1)": "0.101531044267622", "=CHIINV(0.1,2)": "4.60517018598809", "=CHIINV(0.8,2)": "0.446287102628419", + // CHISQ.DIST + "=CHISQ.DIST(0,2,TRUE)": "0", + "=CHISQ.DIST(4,1,TRUE)": "0.954499736103642", + "=CHISQ.DIST(1180,1180,FALSE)": "0.00821093706387967", + "=CHISQ.DIST(2,1,FALSE)": "0.103776874355149", + "=CHISQ.DIST(3,2,FALSE)": "0.111565080074215", + "=CHISQ.DIST(2,3,FALSE)": "0.207553748710297", + "=CHISQ.DIST(1425,1,FALSE)": "3.88315098887099E-312", + "=CHISQ.DIST(3,2,TRUE)": "0.77686983985157", + // CHISQ.DIST.RT + "=CHISQ.DIST.RT(0.5,3)": "0.918891411654676", + "=CHISQ.DIST.RT(8,3)": "0.0460117056892314", + "=CHISQ.DIST.RT(40,4)": "4.32842260712097E-08", + "=CHISQ.DIST.RT(42,4)": "1.66816329414062E-08", // CONFIDENCE "=CONFIDENCE(0.05,0.07,100)": "0.0137197479028414", // CONFIDENCE.NORM @@ -918,6 +932,9 @@ func TestCalcCellValue(t *testing.T) { // GAMMALN "=GAMMALN(4.5)": "2.45373657084244", "=GAMMALN(INT(1))": "0", + // GAMMALN.PRECISE + "=GAMMALN.PRECISE(0.4)": "0.796677817701784", + "=GAMMALN.PRECISE(4.5)": "2.45373657084244", // GAUSS "=GAUSS(-5)": "-0.499999713348428", "=GAUSS(0)": "0", @@ -2523,6 +2540,17 @@ func TestCalcCellValue(t *testing.T) { "=CHIINV(0,1)": "#NUM!", "=CHIINV(2,1)": "#NUM!", "=CHIINV(0.5,0.5)": "#NUM!", + // CHISQ.DIST + "=CHISQ.DIST()": "CHISQ.DIST requires 3 arguments", + "=CHISQ.DIST(\"\",2,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHISQ.DIST(3,\"\",TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHISQ.DIST(3,2,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=CHISQ.DIST(-1,2,TRUE)": "#NUM!", + "=CHISQ.DIST(3,0,TRUE)": "#NUM!", + // CHISQ.DIST.RT + "=CHISQ.DIST.RT()": "CHISQ.DIST.RT requires 2 numeric arguments", + "=CHISQ.DIST.RT(\"\",3)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHISQ.DIST.RT(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // CONFIDENCE "=CONFIDENCE()": "CONFIDENCE requires 3 numeric arguments", "=CONFIDENCE(\"\",0.07,100)": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -2621,6 +2649,9 @@ func TestCalcCellValue(t *testing.T) { "=GAMMALN(F1)": "GAMMALN requires 1 numeric argument", "=GAMMALN(0)": "#N/A", "=GAMMALN(INT(0))": "#N/A", + // GAMMALN.PRECISE + "=GAMMALN.PRECISE()": "GAMMALN.PRECISE requires 1 numeric argument", + "=GAMMALN.PRECISE(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // GAUSS "=GAUSS()": "GAUSS requires 1 numeric argument", "=GAUSS(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", From 0030e800ca1e151483db96172034122c86ce97fc Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 29 Mar 2022 00:03:58 +0800 Subject: [PATCH 016/213] ref #65, new formula functions: F.TEST and FTEST --- calc.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++++--- calc_test.go | 45 +++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index 84f8568245..0f2003dd1e 100644 --- a/calc.go +++ b/calc.go @@ -443,6 +443,8 @@ type formulaFuncs struct { // FLOOR.MATH // FLOOR.PRECISE // FORMULATEXT +// F.TEST +// FTEST // FV // FVSCHEDULE // GAMMA @@ -6468,7 +6470,7 @@ func getGammaContFraction(fA, fX float64) float64 { fQk = math.Nextafter(f3, f3) - math.Nextafter(f4, f4) ) if fQk != 0 { - var fR = fPk / fQk + fR := fPk / fQk bFinished = math.Abs((fApprox-fR)/fR) <= fHalfMachEps fApprox = fR } @@ -6486,8 +6488,8 @@ func getGammaContFraction(fA, fX float64) float64 { // getLogGammaHelper is a part of implementation of the function getLogGamma. func getLogGammaHelper(fZ float64) float64 { - var _fg = 6.024680040776729583740234375 - var zgHelp = fZ + _fg - 0.5 + _fg := 6.024680040776729583740234375 + zgHelp := fZ + _fg - 0.5 return math.Log(getLanczosSum(fZ)) + (fZ-0.5)*math.Log(zgHelp) - zgHelp } @@ -6511,7 +6513,7 @@ func getGammaHelper(fZ float64) float64 { // getLogGamma calculates the natural logarithm of the gamma function. func getLogGamma(fZ float64) float64 { - var fMaxGammaArgument = 171.624376956302 + fMaxGammaArgument := 171.624376956302 if fZ >= fMaxGammaArgument { return getLogGammaHelper(fZ) } @@ -7578,6 +7580,77 @@ func (fn *formulaFuncs) FINV(argsList *list.List) formulaArg { return newNumberFormulaArg((1/calcBetainv(1-(1-probability.Number), d2.Number/2, d1.Number/2, 0, 1) - 1) * (d2.Number / d1.Number)) } +// FdotTEST function returns the F-Test for two supplied arrays. I.e. the +// function returns the two-tailed probability that the variances in the two +// supplied arrays are not significantly different. The syntax of the Ftest +// function is: +// +// F.TEST(array1,array2) +// +func (fn *formulaFuncs) FdotTEST(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "F.TEST requires 2 arguments") + } + array1 := argsList.Front().Value.(formulaArg) + array2 := argsList.Back().Value.(formulaArg) + left, right := array1.ToList(), array2.ToList() + collectMatrix := func(args []formulaArg) (n, accu float64) { + var p, sum float64 + for _, arg := range args { + if num := arg.ToNumber(); num.Type == ArgNumber { + x := num.Number - p + y := x / (n + 1) + p += y + accu += n * x * y + n++ + sum += num.Number + } + } + return + } + nums, accu := collectMatrix(left) + f3 := nums - 1 + if nums == 1 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + f1 := accu / (nums - 1) + if f1 == 0 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + nums, accu = collectMatrix(right) + f4 := nums - 1 + if nums == 1 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + f2 := accu / (nums - 1) + if f2 == 0 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + args := list.New() + args.PushBack(newNumberFormulaArg(f1 / f2)) + args.PushBack(newNumberFormulaArg(f3)) + args.PushBack(newNumberFormulaArg(f4)) + probability := (1 - fn.FDIST(args).Number) * 2 + if probability > 1 { + probability = 2 - probability + } + return newNumberFormulaArg(probability) +} + +// FTEST function returns the F-Test for two supplied arrays. I.e. the function +// returns the two-tailed probability that the variances in the two supplied +// arrays are not significantly different. The syntax of the Ftest function +// is: +// +// FTEST(array1,array2) +// +func (fn *formulaFuncs) FTEST(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "FTEST requires 2 arguments") + } + return fn.FdotTEST(argsList) +} + // LOGINV function calculates the inverse of the Cumulative Log-Normal // Distribution Function of x, for a supplied probability. The syntax of the // function is: diff --git a/calc_test.go b/calc_test.go index 306e24d99c..fb91e2e514 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4292,6 +4292,51 @@ func TestCalcCHITESTandCHISQdotTEST(t *testing.T) { } } +func TestCalcFTEST(t *testing.T) { + cellData := [][]interface{}{ + {"Group 1", "Group 2"}, + {3.5, 9.2}, + {4.7, 8.2}, + {6.2, 7.3}, + {4.9, 6.1}, + {3.8, 5.4}, + {5.5, 7.8}, + {7.1, 5.9}, + {6.7, 8.4}, + {3.9, 7.7}, + {4.6, 6.6}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=FTEST(A2:A11,B2:B11)": "0.95403555939413", + "=F.TEST(A2:A11,B2:B11)": "0.95403555939413", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError := map[string]string{ + "=FTEST()": "FTEST requires 2 arguments", + "=FTEST(A2:A2,B2:B2)": "#DIV/0!", + "=FTEST(A12:A14,B2:B4)": "#DIV/0!", + "=FTEST(A2:A4,B2:B2)": "#DIV/0!", + "=FTEST(A2:A4,B12:B14)": "#DIV/0!", + "=F.TEST()": "F.TEST requires 2 arguments", + "=F.TEST(A2:A2,B2:B2)": "#DIV/0!", + "=F.TEST(A12:A14,B2:B4)": "#DIV/0!", + "=F.TEST(A2:A4,B2:B2)": "#DIV/0!", + "=F.TEST(A2:A4,B12:B14)": "#DIV/0!", + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.EqualError(t, err, expected, formula) + assert.Equal(t, "", result, formula) + } +} + func TestCalcIRR(t *testing.T) { cellData := [][]interface{}{{-1}, {0.2}, {0.24}, {0.288}, {0.3456}, {0.4147}} f := prepareCalcData(cellData) From 29d63f6ae0a3411be7e9799c80e6be41997c4f14 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 30 Mar 2022 00:01:38 +0800 Subject: [PATCH 017/213] ref #65, new formula functions: HYPGEOM.DIST and HYPGEOMDIST --- calc.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 44 +++++++++++++++++++++++++++ cell.go | 9 ++++-- 3 files changed, 136 insertions(+), 3 deletions(-) diff --git a/calc.go b/calc.go index 0f2003dd1e..fc2895f449 100644 --- a/calc.go +++ b/calc.go @@ -464,6 +464,8 @@ type formulaFuncs struct { // HEX2OCT // HLOOKUP // HOUR +// HYPGEOM.DIST +// HYPGEOMDIST // IF // IFERROR // IFNA @@ -7328,6 +7330,90 @@ func (fn *formulaFuncs) HARMEAN(argsList *list.List) formulaArg { return newNumberFormulaArg(1 / (val / cnt)) } +// prepareHYPGEOMDISTArgs checking and prepare arguments for the formula +// function HYPGEOMDIST and HYPGEOM.DIST. +func (fn *formulaFuncs) prepareHYPGEOMDISTArgs(name string, argsList *list.List) formulaArg { + if name == "HYPGEOMDIST" && argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "HYPGEOMDIST requires 4 numeric arguments") + } + if name == "HYPGEOM.DIST" && argsList.Len() != 5 { + return newErrorFormulaArg(formulaErrorVALUE, "HYPGEOM.DIST requires 5 arguments") + } + var sampleS, numberSample, populationS, numberPop, cumulative formulaArg + if sampleS = argsList.Front().Value.(formulaArg).ToNumber(); sampleS.Type != ArgNumber { + return sampleS + } + if numberSample = argsList.Front().Next().Value.(formulaArg).ToNumber(); numberSample.Type != ArgNumber { + return numberSample + } + if populationS = argsList.Front().Next().Next().Value.(formulaArg).ToNumber(); populationS.Type != ArgNumber { + return populationS + } + if numberPop = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); numberPop.Type != ArgNumber { + return numberPop + } + if sampleS.Number < 0 || + sampleS.Number > math.Min(numberSample.Number, populationS.Number) || + sampleS.Number < math.Max(0, numberSample.Number-numberPop.Number+populationS.Number) || + numberSample.Number <= 0 || + numberSample.Number > numberPop.Number || + populationS.Number <= 0 || + populationS.Number > numberPop.Number || + numberPop.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if name == "HYPGEOM.DIST" { + if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type != ArgNumber { + return cumulative + } + } + return newListFormulaArg([]formulaArg{sampleS, numberSample, populationS, numberPop, cumulative}) +} + +// HYPGEOMdotDIST function returns the value of the hypergeometric distribution +// for a specified number of successes from a population sample. The function +// can calculate the cumulative distribution or the probability density +// function. The syntax of the function is: +// +// HYPGEOM.DIST(sample_s,number_sample,population_s,number_pop,cumulative) +// +func (fn *formulaFuncs) HYPGEOMdotDIST(argsList *list.List) formulaArg { + args := fn.prepareHYPGEOMDISTArgs("HYPGEOM.DIST", argsList) + if args.Type != ArgList { + return args + } + sampleS, numberSample, populationS, numberPop, cumulative := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4] + if cumulative.Number == 1 { + var res float64 + for i := 0; i <= int(sampleS.Number); i++ { + res += binomCoeff(populationS.Number, float64(i)) * + binomCoeff(numberPop.Number-populationS.Number, numberSample.Number-float64(i)) / + binomCoeff(numberPop.Number, numberSample.Number) + } + return newNumberFormulaArg(res) + } + return newNumberFormulaArg(binomCoeff(populationS.Number, sampleS.Number) * + binomCoeff(numberPop.Number-populationS.Number, numberSample.Number-sampleS.Number) / + binomCoeff(numberPop.Number, numberSample.Number)) +} + +// HYPGEOMDIST function returns the value of the hypergeometric distribution +// for a given number of successes from a sample of a population. The syntax +// of the function is: +// +// HYPGEOMDIST(sample_s,number_sample,population_s,number_pop) +// +func (fn *formulaFuncs) HYPGEOMDIST(argsList *list.List) formulaArg { + args := fn.prepareHYPGEOMDISTArgs("HYPGEOMDIST", argsList) + if args.Type != ArgList { + return args + } + sampleS, numberSample, populationS, numberPop := args.List[0], args.List[1], args.List[2], args.List[3] + return newNumberFormulaArg(binomCoeff(populationS.Number, sampleS.Number) * + binomCoeff(numberPop.Number-populationS.Number, numberSample.Number-sampleS.Number) / + binomCoeff(numberPop.Number, numberSample.Number)) +} + // KURT function calculates the kurtosis of a supplied set of values. The // syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index fb91e2e514..30db8cfad3 100644 --- a/calc_test.go +++ b/calc_test.go @@ -945,6 +945,20 @@ func TestCalcCellValue(t *testing.T) { // HARMEAN "=HARMEAN(2.5,3,0.5,1,3)": "1.22950819672131", "=HARMEAN(\"2.5\",3,0.5,1,INT(3),\"\")": "1.22950819672131", + // HYPGEOM.DIST + "=HYPGEOM.DIST(0,3,3,9,TRUE)": "0.238095238095238", + "=HYPGEOM.DIST(1,3,3,9,TRUE)": "0.773809523809524", + "=HYPGEOM.DIST(2,3,3,9,TRUE)": "0.988095238095238", + "=HYPGEOM.DIST(3,3,3,9,TRUE)": "1", + "=HYPGEOM.DIST(1,4,4,12,FALSE)": "0.452525252525253", + "=HYPGEOM.DIST(2,4,4,12,FALSE)": "0.339393939393939", + "=HYPGEOM.DIST(3,4,4,12,FALSE)": "0.0646464646464646", + "=HYPGEOM.DIST(4,4,4,12,FALSE)": "0.00202020202020202", + // HYPGEOMDIST + "=HYPGEOMDIST(1,4,4,12)": "0.452525252525253", + "=HYPGEOMDIST(2,4,4,12)": "0.339393939393939", + "=HYPGEOMDIST(3,4,4,12)": "0.0646464646464646", + "=HYPGEOMDIST(4,4,4,12)": "0.00202020202020202", // KURT "=KURT(F1:F9)": "-1.03350350255137", "=KURT(F1,F2:F9)": "-1.03350350255137", @@ -2652,6 +2666,7 @@ func TestCalcCellValue(t *testing.T) { // GAMMALN.PRECISE "=GAMMALN.PRECISE()": "GAMMALN.PRECISE requires 1 numeric argument", "=GAMMALN.PRECISE(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=GAMMALN.PRECISE(0)": "#NUM!", // GAUSS "=GAUSS()": "GAUSS requires 1 numeric argument", "=GAUSS(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -2663,6 +2678,35 @@ func TestCalcCellValue(t *testing.T) { "=HARMEAN()": "HARMEAN requires at least 1 argument", "=HARMEAN(-1)": "#N/A", "=HARMEAN(0)": "#N/A", + // HYPGEOM.DIST + "=HYPGEOM.DIST()": "HYPGEOM.DIST requires 5 arguments", + "=HYPGEOM.DIST(\"\",4,4,12,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=HYPGEOM.DIST(1,\"\",4,12,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=HYPGEOM.DIST(1,4,\"\",12,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=HYPGEOM.DIST(1,4,4,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=HYPGEOM.DIST(1,4,4,12,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=HYPGEOM.DIST(-1,4,4,12,FALSE)": "#NUM!", + "=HYPGEOM.DIST(2,1,4,12,FALSE)": "#NUM!", + "=HYPGEOM.DIST(2,4,1,12,FALSE)": "#NUM!", + "=HYPGEOM.DIST(2,2,2,1,FALSE)": "#NUM!", + "=HYPGEOM.DIST(1,0,4,12,FALSE)": "#NUM!", + "=HYPGEOM.DIST(1,4,4,2,FALSE)": "#NUM!", + "=HYPGEOM.DIST(1,4,0,12,FALSE)": "#NUM!", + "=HYPGEOM.DIST(1,4,4,0,FALSE)": "#NUM!", + // HYPGEOMDIST + "=HYPGEOMDIST()": "HYPGEOMDIST requires 4 numeric arguments", + "=HYPGEOMDIST(\"\",4,4,12)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=HYPGEOMDIST(1,\"\",4,12)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=HYPGEOMDIST(1,4,\"\",12)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=HYPGEOMDIST(1,4,4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=HYPGEOMDIST(-1,4,4,12)": "#NUM!", + "=HYPGEOMDIST(2,1,4,12)": "#NUM!", + "=HYPGEOMDIST(2,4,1,12)": "#NUM!", + "=HYPGEOMDIST(2,2,2,1)": "#NUM!", + "=HYPGEOMDIST(1,0,4,12)": "#NUM!", + "=HYPGEOMDIST(1,4,4,2)": "#NUM!", + "=HYPGEOMDIST(1,4,0,12)": "#NUM!", + "=HYPGEOMDIST(1,4,4,0)": "#NUM!", // KURT "=KURT()": "KURT requires at least 1 argument", "=KURT(F1,INT(1))": "#DIV/0!", diff --git a/cell.go b/cell.go index 4b76271a70..b2818e7fbd 100644 --- a/cell.go +++ b/cell.go @@ -672,9 +672,12 @@ type HyperlinkOpts struct { // SetCellHyperLink provides a function to set cell hyperlink by given // worksheet name and link URL address. LinkType defines two types of -// hyperlink "External" for website or "Location" for moving to one of cell -// in this workbook. Maximum limit hyperlinks in a worksheet is 65530. The -// below is example for external link. +// hyperlink "External" for website or "Location" for moving to one of cell in +// this workbook. Maximum limit hyperlinks in a worksheet is 65530. This +// function is only used to set the hyperlink of the cell and doesn't affect +// the value of the cell. If you need to set the value of the cell, please use +// the other functions such as `SetCellStyle` or `SetSheetRow`. The below is +// example for external link. // // if err := f.SetCellHyperLink("Sheet1", "A3", // "https://github.com/xuri/excelize", "External"); err != nil { From 18c48d829133ec395bda8440a04d9f25dcfe11f5 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 31 Mar 2022 00:04:40 +0800 Subject: [PATCH 018/213] ref #65, new formula functions: CHISQ.INV and CHISQ.INV.RT --- calc.go | 154 +++++++++++++++++++++++++++++++++++++++++++++++---- calc_test.go | 25 +++++++++ 2 files changed, 169 insertions(+), 10 deletions(-) diff --git a/calc.go b/calc.go index fc2895f449..f279b348b7 100644 --- a/calc.go +++ b/calc.go @@ -359,6 +359,8 @@ type formulaFuncs struct { // CHITEST // CHISQ.DIST // CHISQ.DIST.RT +// CHISQ.INV +// CHISQ.INV.RT // CHISQ.TEST // CHOOSE // CLEAN @@ -6631,6 +6633,132 @@ func (fn *formulaFuncs) CHISQdotTEST(argsList *list.List) formulaArg { return fn.CHITEST(argsList) } +// hasChangeOfSign check if the sign has been changed. +func hasChangeOfSign(u, w float64) bool { + return (u < 0 && w > 0) || (u > 0 && w < 0) +} + +// calcInverseIterator directly maps the required parameters for inverse +// distribution functions. +type calcInverseIterator struct { + name string + fp, fDF float64 +} + +// chiSqDist implements inverse distribution with left tail for the Chi-Square +// distribution. +func (iterator *calcInverseIterator) chiSqDist(x float64) float64 { + return iterator.fp - getChiSqDistCDF(x, iterator.fDF) +} + +// inverseQuadraticInterpolation inverse quadratic interpolation with +// additional brackets. +func inverseQuadraticInterpolation(iterator calcInverseIterator, fAx, fAy, fBx, fBy float64) float64 { + fYEps := 1.0e-307 + fXEps := 2.22045e-016 + fPx, fPy, fQx, fQy, fRx, fRy := fAx, fAy, fBx, fBy, fAx, fAy + fSx := 0.5 * (fAx + fBx) + bHasToInterpolate := true + nCount := 0 + for nCount < 500 && math.Abs(fRy) > fYEps && (fBx-fAx) > math.Max(math.Abs(fAx), math.Abs(fBx))*fXEps { + if bHasToInterpolate { + if fPy != fQy && fQy != fRy && fRy != fPy { + fSx = fPx*fRy*fQy/(fRy-fPy)/(fQy-fPy) + fRx*fQy*fPy/(fQy-fRy)/(fPy-fRy) + + fQx*fPy*fRy/(fPy-fQy)/(fRy-fQy) + bHasToInterpolate = (fAx < fSx) && (fSx < fBx) + } else { + bHasToInterpolate = false + } + } + if !bHasToInterpolate { + fSx = 0.5 * (fAx + fBx) + fQx, fQy = fBx, fBy + bHasToInterpolate = true + } + fPx, fQx, fRx, fPy, fQy = fQx, fRx, fSx, fQy, fRy + fRy = iterator.chiSqDist(fSx) + if hasChangeOfSign(fAy, fRy) { + fBx, fBy = fRx, fRy + } else { + fAx, fAy = fRx, fRy + } + bHasToInterpolate = bHasToInterpolate && (math.Abs(fRy)*2 <= math.Abs(fQy)) + nCount++ + } + return fRx +} + +// calcIterateInverse function calculates the iteration for inverse +// distributions. +func calcIterateInverse(iterator calcInverseIterator, fAx, fBx float64) float64 { + fAy, fBy := iterator.chiSqDist(fAx), iterator.chiSqDist(fBx) + var fTemp float64 + var nCount int + for nCount = 0; nCount < 1000 && !hasChangeOfSign(fAy, fBy); nCount++ { + if math.Abs(fAy) <= math.Abs(fBy) { + fTemp = fAx + fAx += 2 * (fAx - fBx) + if fAx < 0 { + fAx = 0 + } + fBx = fTemp + fBy = fAy + fAy = iterator.chiSqDist(fAx) + } else { + fTemp = fBx + fBx += 2 * (fBx - fAx) + fAx = fTemp + fAy = fBy + fBy = iterator.chiSqDist(fBx) + } + } + if fAy == 0 || fBy == 0 { + return 0 + } + return inverseQuadraticInterpolation(iterator, fAx, fAy, fBx, fBy) +} + +// CHISQdotINV function calculates the inverse of the left-tailed probability +// of the Chi-Square Distribution. The syntax of the function is: +// +// CHISQ.INV(probability,degrees_freedom) +// +func (fn *formulaFuncs) CHISQdotINV(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.INV requires 2 numeric arguments") + } + var probability, degrees formulaArg + if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { + return probability + } + if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber { + return degrees + } + if probability.Number < 0 || probability.Number >= 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if degrees.Number < 1 || degrees.Number > math.Pow10(10) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(calcIterateInverse(calcInverseIterator{ + name: "CHISQ.INV", + fp: probability.Number, + fDF: degrees.Number, + }, degrees.Number/2, degrees.Number)) +} + +// CHISQdotINVdotRT function calculates the inverse of the right-tailed +// probability of the Chi-Square Distribution. The syntax of the function is: +// +// CHISQ.INV.RT(probability,degrees_freedom) +// +func (fn *formulaFuncs) CHISQdotINVdotRT(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.INV.RT requires 2 numeric arguments") + } + return fn.CHIINV(argsList) +} + // confidence is an implementation of the formula functions CONFIDENCE and // CONFIDENCE.NORM. func (fn *formulaFuncs) confidence(name string, argsList *list.List) formulaArg { @@ -7330,8 +7458,21 @@ func (fn *formulaFuncs) HARMEAN(argsList *list.List) formulaArg { return newNumberFormulaArg(1 / (val / cnt)) } -// prepareHYPGEOMDISTArgs checking and prepare arguments for the formula -// function HYPGEOMDIST and HYPGEOM.DIST. +// checkHYPGEOMDISTArgs checking arguments for the formula function HYPGEOMDIST +// and HYPGEOM.DIST. +func checkHYPGEOMDISTArgs(sampleS, numberSample, populationS, numberPop formulaArg) bool { + return sampleS.Number < 0 || + sampleS.Number > math.Min(numberSample.Number, populationS.Number) || + sampleS.Number < math.Max(0, numberSample.Number-numberPop.Number+populationS.Number) || + numberSample.Number <= 0 || + numberSample.Number > numberPop.Number || + populationS.Number <= 0 || + populationS.Number > numberPop.Number || + numberPop.Number <= 0 +} + +// prepareHYPGEOMDISTArgs prepare arguments for the formula function +// HYPGEOMDIST and HYPGEOM.DIST. func (fn *formulaFuncs) prepareHYPGEOMDISTArgs(name string, argsList *list.List) formulaArg { if name == "HYPGEOMDIST" && argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "HYPGEOMDIST requires 4 numeric arguments") @@ -7352,14 +7493,7 @@ func (fn *formulaFuncs) prepareHYPGEOMDISTArgs(name string, argsList *list.List) if numberPop = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); numberPop.Type != ArgNumber { return numberPop } - if sampleS.Number < 0 || - sampleS.Number > math.Min(numberSample.Number, populationS.Number) || - sampleS.Number < math.Max(0, numberSample.Number-numberPop.Number+populationS.Number) || - numberSample.Number <= 0 || - numberSample.Number > numberPop.Number || - populationS.Number <= 0 || - populationS.Number > numberPop.Number || - numberPop.Number <= 0 { + if checkHYPGEOMDISTArgs(sampleS, numberSample, populationS, numberPop) { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } if name == "HYPGEOM.DIST" { diff --git a/calc_test.go b/calc_test.go index 30db8cfad3..43f6a29b5f 100644 --- a/calc_test.go +++ b/calc_test.go @@ -865,6 +865,16 @@ func TestCalcCellValue(t *testing.T) { "=CHISQ.DIST.RT(8,3)": "0.0460117056892314", "=CHISQ.DIST.RT(40,4)": "4.32842260712097E-08", "=CHISQ.DIST.RT(42,4)": "1.66816329414062E-08", + // CHISQ.INV + "=CHISQ.INV(0,2)": "0", + "=CHISQ.INV(0.75,1)": "1.32330369693147", + "=CHISQ.INV(0.1,2)": "0.210721031315653", + "=CHISQ.INV(0.8,2)": "3.2188758248682", + "=CHISQ.INV(0.25,3)": "1.21253290304567", + // CHISQ.INV.RT + "=CHISQ.INV.RT(0.75,1)": "0.101531044267622", + "=CHISQ.INV.RT(0.1,2)": "4.60517018598809", + "=CHISQ.INV.RT(0.8,2)": "0.446287102628419", // CONFIDENCE "=CONFIDENCE(0.05,0.07,100)": "0.0137197479028414", // CONFIDENCE.NORM @@ -2565,6 +2575,21 @@ func TestCalcCellValue(t *testing.T) { "=CHISQ.DIST.RT()": "CHISQ.DIST.RT requires 2 numeric arguments", "=CHISQ.DIST.RT(\"\",3)": "strconv.ParseFloat: parsing \"\": invalid syntax", "=CHISQ.DIST.RT(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + // CHISQ.INV + "=CHISQ.INV()": "CHISQ.INV requires 2 numeric arguments", + "=CHISQ.INV(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHISQ.INV(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHISQ.INV(-1,1)": "#NUM!", + "=CHISQ.INV(1,1)": "#NUM!", + "=CHISQ.INV(0.5,0.5)": "#NUM!", + "=CHISQ.INV(0.5,10000000001)": "#NUM!", + // CHISQ.INV.RT + "=CHISQ.INV.RT()": "CHISQ.INV.RT requires 2 numeric arguments", + "=CHISQ.INV.RT(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHISQ.INV.RT(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHISQ.INV.RT(0,1)": "#NUM!", + "=CHISQ.INV.RT(2,1)": "#NUM!", + "=CHISQ.INV.RT(0.5,0.5)": "#NUM!", // CONFIDENCE "=CONFIDENCE()": "CONFIDENCE requires 3 numeric arguments", "=CONFIDENCE(\"\",0.07,100)": "strconv.ParseFloat: parsing \"\": invalid syntax", From d9b5afc1ac4e085b7f2e6838cb13df6ae6962b7f Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 1 Apr 2022 00:09:36 +0800 Subject: [PATCH 019/213] ref #65, new formula functions: NEGBINOM.DIST and NEGBINOMDIST --- calc.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 29 ++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/calc.go b/calc.go index f279b348b7..5bad72abe8 100644 --- a/calc.go +++ b/calc.go @@ -555,6 +555,8 @@ type formulaFuncs struct { // MUNIT // N // NA +// NEGBINOM.DIST +// NEGBINOMDIST // NOMINAL // NORM.DIST // NORMDIST @@ -7983,6 +7985,67 @@ func (fn *formulaFuncs) LOGNORMDIST(argsList *list.List) formulaArg { return fn.NORMSDIST(args) } +// NEGBINOMdotDIST function calculates the probability mass function or the +// cumulative distribution function for the Negative Binomial Distribution. +// This gives the probability that there will be a given number of failures +// before a required number of successes is achieved. The syntax of the +// function is: +// +// NEGBINOM.DIST(number_f,number_s,probability_s,cumulative) +// +func (fn *formulaFuncs) NEGBINOMdotDIST(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "NEGBINOM.DIST requires 4 arguments") + } + var f, s, probability, cumulative formulaArg + if f = argsList.Front().Value.(formulaArg).ToNumber(); f.Type != ArgNumber { + return f + } + if s = argsList.Front().Next().Value.(formulaArg).ToNumber(); s.Type != ArgNumber { + return s + } + if probability = argsList.Front().Next().Next().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { + return probability + } + if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type != ArgNumber { + return cumulative + } + if f.Number < 0 || s.Number < 1 || probability.Number < 0 || probability.Number > 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if cumulative.Number == 1 { + return newNumberFormulaArg(1 - getBetaDist(1-probability.Number, f.Number+1, s.Number)) + } + return newNumberFormulaArg(binomCoeff(f.Number+s.Number-1, s.Number-1) * math.Pow(probability.Number, s.Number) * math.Pow(1-probability.Number, f.Number)) +} + +// NEGBINOMDIST function calculates the Negative Binomial Distribution for a +// given set of parameters. This gives the probability that there will be a +// specified number of failures before a required number of successes is +// achieved. The syntax of the function is: +// +// NEGBINOMDIST(number_f,number_s,probability_s) +// +func (fn *formulaFuncs) NEGBINOMDIST(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "NEGBINOMDIST requires 3 arguments") + } + var f, s, probability formulaArg + if f = argsList.Front().Value.(formulaArg).ToNumber(); f.Type != ArgNumber { + return f + } + if s = argsList.Front().Next().Value.(formulaArg).ToNumber(); s.Type != ArgNumber { + return s + } + if probability = argsList.Back().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { + return probability + } + if f.Number < 0 || s.Number < 1 || probability.Number < 0 || probability.Number > 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(binomCoeff(f.Number+s.Number-1, s.Number-1) * math.Pow(probability.Number, s.Number) * math.Pow(1-probability.Number, f.Number)) +} + // NORMdotDIST function calculates the Normal Probability Density Function or // the Cumulative Normal Distribution. Function for a supplied set of // parameters. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 43f6a29b5f..2b76ed3da8 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1015,6 +1015,16 @@ func TestCalcCellValue(t *testing.T) { "=LOGNORM.DIST(12,10,5,TRUE)": "0.0664171147992078", // LOGNORMDIST "=LOGNORMDIST(12,10,5)": "0.0664171147992078", + // NEGBINOM.DIST + "=NEGBINOM.DIST(6,12,0.5,FALSE)": "0.047210693359375", + "=NEGBINOM.DIST(12,12,0.5,FALSE)": "0.0805901288986206", + "=NEGBINOM.DIST(15,12,0.5,FALSE)": "0.057564377784729", + "=NEGBINOM.DIST(12,12,0.5,TRUE)": "0.580590128898621", + "=NEGBINOM.DIST(15,12,0.5,TRUE)": "0.778965830802917", + // NEGBINOMDIST + "=NEGBINOMDIST(6,12,0.5)": "0.047210693359375", + "=NEGBINOMDIST(12,12,0.5)": "0.0805901288986206", + "=NEGBINOMDIST(15,12,0.5)": "0.057564377784729", // NORM.DIST "=NORM.DIST(0.8,1,0.3,TRUE)": "0.252492537546923", "=NORM.DIST(50,40,20,FALSE)": "0.017603266338215", @@ -2835,6 +2845,25 @@ func TestCalcCellValue(t *testing.T) { "=LOGNORMDIST(12,10,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=LOGNORMDIST(0,2,5)": "#NUM!", "=LOGNORMDIST(12,10,0)": "#NUM!", + // NEGBINOM.DIST + "=NEGBINOM.DIST()": "NEGBINOM.DIST requires 4 arguments", + "=NEGBINOM.DIST(\"\",12,0.5,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=NEGBINOM.DIST(6,\"\",0.5,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=NEGBINOM.DIST(6,12,\"\",TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=NEGBINOM.DIST(6,12,0.5,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=NEGBINOM.DIST(-1,12,0.5,TRUE)": "#NUM!", + "=NEGBINOM.DIST(6,0,0.5,TRUE)": "#NUM!", + "=NEGBINOM.DIST(6,12,-1,TRUE)": "#NUM!", + "=NEGBINOM.DIST(6,12,2,TRUE)": "#NUM!", + // NEGBINOMDIST + "=NEGBINOMDIST()": "NEGBINOMDIST requires 3 arguments", + "=NEGBINOMDIST(\"\",12,0.5)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=NEGBINOMDIST(6,\"\",0.5)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=NEGBINOMDIST(6,12,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=NEGBINOMDIST(-1,12,0.5)": "#NUM!", + "=NEGBINOMDIST(6,0,0.5)": "#NUM!", + "=NEGBINOMDIST(6,12,-1)": "#NUM!", + "=NEGBINOMDIST(6,12,2)": "#NUM!", // NORM.DIST "=NORM.DIST()": "NORM.DIST requires 4 arguments", // NORMDIST From b8345731a477633bc82216dbc398faecafaf894f Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 2 Apr 2022 00:04:21 +0800 Subject: [PATCH 020/213] ref #65, new formula functions: T.DIST and TDIST --- calc.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 23 ++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/calc.go b/calc.go index 5bad72abe8..6252363fda 100644 --- a/calc.go +++ b/calc.go @@ -657,6 +657,8 @@ type formulaFuncs struct { // TBILLEQ // TBILLPRICE // TBILLYIELD +// T.DIST +// TDIST // TEXTJOIN // TIME // TIMEVALUE @@ -9008,6 +9010,94 @@ func (fn *formulaFuncs) STDEVdotP(argsList *list.List) formulaArg { return fn.stdevp("STDEV.P", argsList) } +// getTDist is an implementation for the beta distribution probability density +// function. +func getTDist(T, fDF, nType float64) float64 { + var res float64 + switch nType { + case 1: + res = 0.5 * getBetaDist(fDF/(fDF+T*T), fDF/2, 0.5) + break + case 2: + res = getBetaDist(fDF/(fDF+T*T), fDF/2, 0.5) + break + case 3: + res = math.Pow(1+(T*T/fDF), -(fDF+1)/2) / (math.Sqrt(fDF) * getBeta(0.5, fDF/2.0)) + break + case 4: + X := fDF / (T*T + fDF) + R := 0.5 * getBetaDist(X, 0.5*fDF, 0.5) + res = 1 - R + if T < 0 { + res = R + } + break + } + return res +} + +// TdotDIST function calculates the one-tailed Student's T Distribution, which +// is a continuous probability distribution that is frequently used for +// testing hypotheses on small sample data sets. The syntax of the function +// is: +// +// T.DIST(x,degrees_freedom,cumulative) +// +func (fn *formulaFuncs) TdotDIST(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "T.DIST requires 3 arguments") + } + var x, degrees, cumulative formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if degrees = argsList.Front().Next().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber { + return degrees + } + if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type != ArgNumber { + return cumulative + } + if cumulative.Number == 1 && degrees.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if cumulative.Number == 0 { + if degrees.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if degrees.Number == 0 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + return newNumberFormulaArg(getTDist(x.Number, degrees.Number, 3)) + } + return newNumberFormulaArg(getTDist(x.Number, degrees.Number, 4)) +} + +// TDIST function calculates the Student's T Distribution, which is a +// continuous probability distribution that is frequently used for testing +// hypotheses on small sample data sets. The syntax of the function is: +// +// TDIST(x,degrees_freedom,tails) +// +func (fn *formulaFuncs) TDIST(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "TDIST requires 3 arguments") + } + var x, degrees, tails formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if degrees = argsList.Front().Next().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber { + return degrees + } + if tails = argsList.Back().Value.(formulaArg).ToNumber(); tails.Type != ArgNumber { + return tails + } + if x.Number < 0 || degrees.Number < 1 || (tails.Number != 1 && tails.Number != 2) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(getTDist(x.Number, degrees.Number, tails.Number)) +} + // TRIMMEAN function calculates the trimmed mean (or truncated mean) of a // supplied set of values. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 2b76ed3da8..321934f8c7 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1155,6 +1155,13 @@ func TestCalcCellValue(t *testing.T) { "=STDEVP(A1:B2,6,-1)": "2.40947204913349", // STDEV.P "=STDEV.P(A1:B2,6,-1)": "2.40947204913349", + // T.DIST + "=T.DIST(1,10,TRUE)": "0.82955343384897", + "=T.DIST(-1,10,TRUE)": "0.17044656615103", + "=T.DIST(-1,10,FALSE)": "0.230361989229139", + // TDIST + "=TDIST(1,10,1)": "0.17044656615103", + "=TDIST(1,10,2)": "0.34089313230206", // TRIMMEAN "=TRIMMEAN(A1:B4,10%)": "2.5", "=TRIMMEAN(A1:B4,70%)": "2.5", @@ -3009,6 +3016,22 @@ func TestCalcCellValue(t *testing.T) { // STDEV.P "=STDEV.P()": "STDEV.P requires at least 1 argument", "=STDEV.P(\"\")": "#DIV/0!", + // T.DIST + "=T.DIST()": "T.DIST requires 3 arguments", + "=T.DIST(\"\",10,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.DIST(1,\"\",TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.DIST(1,10,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=T.DIST(1,0,TRUE)": "#NUM!", + "=T.DIST(1,-1,FALSE)": "#NUM!", + "=T.DIST(1,0,FALSE)": "#DIV/0!", + // TDIST + "=TDIST()": "TDIST requires 3 arguments", + "=TDIST(\"\",10,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TDIST(1,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TDIST(1,10,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TDIST(-1,10,1)": "#NUM!", + "=TDIST(1,0,1)": "#NUM!", + "=TDIST(1,10,0)": "#NUM!", // TRIMMEAN "=TRIMMEAN()": "TRIMMEAN requires 2 arguments", "=TRIMMEAN(A1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", From be8fc0a4c5795bb793b171c25fd90e0369812a05 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 3 Apr 2022 01:18:32 +0800 Subject: [PATCH 021/213] ref #65, new formula functions: T.DIST.2T and T.DIST.RT - Update GitHub Action settings --- .github/workflows/codeql-analysis.yml | 44 ++-------------------- .github/workflows/go.yml | 4 +- calc.go | 54 +++++++++++++++++++++++++++ calc_test.go | 16 ++++++++ 4 files changed, 76 insertions(+), 42 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9dddb5771b..c62270a94d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,15 +1,9 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. name: "CodeQL" on: push: branches: [master] pull_request: - # The branches below must be a subset of the branches above branches: [master] schedule: - cron: '0 6 * * 3' @@ -22,50 +16,20 @@ jobs: strategy: fail-fast: false matrix: - # Override automatic language detection by changing the below list - # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] language: ['go'] - # Learn more... - # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release + uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8310222581..bc5db46b9b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -14,12 +14,12 @@ jobs: steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Get dependencies run: | diff --git a/calc.go b/calc.go index 6252363fda..ad62596cc1 100644 --- a/calc.go +++ b/calc.go @@ -658,6 +658,8 @@ type formulaFuncs struct { // TBILLPRICE // TBILLYIELD // T.DIST +// T.DIST.2T +// T.DIST.RT // TDIST // TEXTJOIN // TIME @@ -9072,6 +9074,58 @@ func (fn *formulaFuncs) TdotDIST(argsList *list.List) formulaArg { return newNumberFormulaArg(getTDist(x.Number, degrees.Number, 4)) } +// TdotDISTdot2T function calculates the two-tailed Student's T Distribution, +// which is a continuous probability distribution that is frequently used for +// testing hypotheses on small sample data sets. The syntax of the function +// is: +// +// T.DIST.2T(x,degrees_freedom) +// +func (fn *formulaFuncs) TdotDISTdot2T(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "T.DIST.2T requires 2 arguments") + } + var x, degrees formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber { + return degrees + } + if x.Number < 0 || degrees.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(getTDist(x.Number, degrees.Number, 2)) +} + +// TdotDISTdotRT function calculates the right-tailed Student's T Distribution, +// which is a continuous probability distribution that is frequently used for +// testing hypotheses on small sample data sets. The syntax of the function +// is: +// +// T.DIST.RT(x,degrees_freedom) +// +func (fn *formulaFuncs) TdotDISTdotRT(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "T.DIST.RT requires 2 arguments") + } + var x, degrees formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber { + return degrees + } + if degrees.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + v := getTDist(x.Number, degrees.Number, 1) + if x.Number < 0 { + v = 1 - v + } + return newNumberFormulaArg(v) +} + // TDIST function calculates the Student's T Distribution, which is a // continuous probability distribution that is frequently used for testing // hypotheses on small sample data sets. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 321934f8c7..632434454a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1159,6 +1159,11 @@ func TestCalcCellValue(t *testing.T) { "=T.DIST(1,10,TRUE)": "0.82955343384897", "=T.DIST(-1,10,TRUE)": "0.17044656615103", "=T.DIST(-1,10,FALSE)": "0.230361989229139", + // T.DIST.2T + "=T.DIST.2T(1,10)": "0.34089313230206", + // T.DIST.RT + "=T.DIST.RT(1,10)": "0.17044656615103", + "=T.DIST.RT(-1,10)": "0.82955343384897", // TDIST "=TDIST(1,10,1)": "0.17044656615103", "=TDIST(1,10,2)": "0.34089313230206", @@ -3024,6 +3029,17 @@ func TestCalcCellValue(t *testing.T) { "=T.DIST(1,0,TRUE)": "#NUM!", "=T.DIST(1,-1,FALSE)": "#NUM!", "=T.DIST(1,0,FALSE)": "#DIV/0!", + // T.DIST.2T + "=T.DIST.2T()": "T.DIST.2T requires 2 arguments", + "=T.DIST.2T(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.DIST.2T(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.DIST.2T(-1,10)": "#NUM!", + "=T.DIST.2T(1,0)": "#NUM!", + // T.DIST.RT + "=T.DIST.RT()": "T.DIST.RT requires 2 arguments", + "=T.DIST.RT(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.DIST.RT(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.DIST.RT(1,0)": "#NUM!", // TDIST "=TDIST()": "TDIST requires 3 arguments", "=TDIST(\"\",10,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", From ecbc6e2fde1941cb5ac9e5f3bfce329e7bfa8825 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 4 Apr 2022 00:21:33 +0800 Subject: [PATCH 022/213] ref #65, new formula functions: T.INV and T.INV.2T - Typo fixed --- calc.go | 111 +++++++++++++++++++++++++++++++++++++-------------- calc_test.go | 19 +++++++++ file.go | 4 +- lib.go | 47 ++++++++++++++++------ numfmt.go | 10 ++--- 5 files changed, 140 insertions(+), 51 deletions(-) diff --git a/calc.go b/calc.go index ad62596cc1..c375b71642 100644 --- a/calc.go +++ b/calc.go @@ -664,6 +664,8 @@ type formulaFuncs struct { // TEXTJOIN // TIME // TIMEVALUE +// T.INV +// T.INV.2T // TODAY // TRANSPOSE // TRIM @@ -1265,27 +1267,6 @@ func isOperand(token efp.Token) bool { return token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText) } -// getDefinedNameRefTo convert defined name to reference range. -func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string) (refTo string) { - var workbookRefTo, worksheetRefTo string - for _, definedName := range f.GetDefinedName() { - if definedName.Name == definedNameName { - // worksheet scope takes precedence over scope workbook when both definedNames exist - if definedName.Scope == "Workbook" { - workbookRefTo = definedName.RefersTo - } - if definedName.Scope == currentSheet { - worksheetRefTo = definedName.RefersTo - } - } - } - refTo = workbookRefTo - if worksheetRefTo != "" { - refTo = worksheetRefTo - } - return -} - // parseToken parse basic arithmetic operator priority and evaluate based on // operators and operands. func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error { @@ -6647,14 +6628,16 @@ func hasChangeOfSign(u, w float64) bool { // calcInverseIterator directly maps the required parameters for inverse // distribution functions. type calcInverseIterator struct { - name string - fp, fDF float64 + name string + fp, fDF, nT float64 } -// chiSqDist implements inverse distribution with left tail for the Chi-Square -// distribution. -func (iterator *calcInverseIterator) chiSqDist(x float64) float64 { - return iterator.fp - getChiSqDistCDF(x, iterator.fDF) +// callBack implements the callback function for the inverse iterator. +func (iterator *calcInverseIterator) callBack(x float64) float64 { + if iterator.name == "CHISQ.INV" { + return iterator.fp - getChiSqDistCDF(x, iterator.fDF) + } + return iterator.fp - getTDist(x, iterator.fDF, iterator.nT) } // inverseQuadraticInterpolation inverse quadratic interpolation with @@ -6682,7 +6665,7 @@ func inverseQuadraticInterpolation(iterator calcInverseIterator, fAx, fAy, fBx, bHasToInterpolate = true } fPx, fQx, fRx, fPy, fQy = fQx, fRx, fSx, fQy, fRy - fRy = iterator.chiSqDist(fSx) + fRy = iterator.callBack(fSx) if hasChangeOfSign(fAy, fRy) { fBx, fBy = fRx, fRy } else { @@ -6697,7 +6680,7 @@ func inverseQuadraticInterpolation(iterator calcInverseIterator, fAx, fAy, fBx, // calcIterateInverse function calculates the iteration for inverse // distributions. func calcIterateInverse(iterator calcInverseIterator, fAx, fBx float64) float64 { - fAy, fBy := iterator.chiSqDist(fAx), iterator.chiSqDist(fBx) + fAy, fBy := iterator.callBack(fAx), iterator.callBack(fBx) var fTemp float64 var nCount int for nCount = 0; nCount < 1000 && !hasChangeOfSign(fAy, fBy); nCount++ { @@ -6709,13 +6692,13 @@ func calcIterateInverse(iterator calcInverseIterator, fAx, fBx float64) float64 } fBx = fTemp fBy = fAy - fAy = iterator.chiSqDist(fAx) + fAy = iterator.callBack(fAx) } else { fTemp = fBx fBx += 2 * (fBx - fAx) fAx = fTemp fAy = fBy - fBy = iterator.chiSqDist(fBx) + fBy = iterator.callBack(fBx) } } if fAy == 0 || fBy == 0 { @@ -9152,6 +9135,72 @@ func (fn *formulaFuncs) TDIST(argsList *list.List) formulaArg { return newNumberFormulaArg(getTDist(x.Number, degrees.Number, tails.Number)) } +// TdotINV function calculates the left-tailed inverse of the Student's T +// Distribution, which is a continuous probability distribution that is +// frequently used for testing hypotheses on small sample data sets. The +// syntax of the function is: +// +// T.INV(probability,degrees_freedom) +// +func (fn *formulaFuncs) TdotINV(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "T.INV requires 2 arguments") + } + var probability, degrees formulaArg + if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { + return probability + } + if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber { + return degrees + } + if probability.Number <= 0 || probability.Number >= 1 || degrees.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if probability.Number < 0.5 { + return newNumberFormulaArg(-calcIterateInverse(calcInverseIterator{ + name: "T.INV", + fp: 1 - probability.Number, + fDF: degrees.Number, + nT: 4, + }, degrees.Number/2, degrees.Number)) + } + return newNumberFormulaArg(calcIterateInverse(calcInverseIterator{ + name: "T.INV", + fp: probability.Number, + fDF: degrees.Number, + nT: 4, + }, degrees.Number/2, degrees.Number)) +} + +// TdotINVdot2T function calculates the inverse of the two-tailed Student's T +// Distribution, which is a continuous probability distribution that is +// frequently used for testing hypotheses on small sample data sets. The +// syntax of the function is: +// +// T.INV.2T(probability,degrees_freedom) +// +func (fn *formulaFuncs) TdotINVdot2T(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "T.INV.2T requires 2 arguments") + } + var probability, degrees formulaArg + if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { + return probability + } + if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber { + return degrees + } + if probability.Number <= 0 || probability.Number > 1 || degrees.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(calcIterateInverse(calcInverseIterator{ + name: "T.INV.2T", + fp: probability.Number, + fDF: degrees.Number, + nT: 2, + }, degrees.Number/2, degrees.Number)) +} + // TRIMMEAN function calculates the trimmed mean (or truncated mean) of a // supplied set of values. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 632434454a..8565038d9c 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1167,6 +1167,12 @@ func TestCalcCellValue(t *testing.T) { // TDIST "=TDIST(1,10,1)": "0.17044656615103", "=TDIST(1,10,2)": "0.34089313230206", + // T.INV + "=T.INV(0.25,10)": "-0.699812061312432", + "=T.INV(0.75,10)": "0.699812061312432", + // T.INV.2T + "=T.INV.2T(1,10)": "0", + "=T.INV.2T(0.5,10)": "0.699812061312432", // TRIMMEAN "=TRIMMEAN(A1:B4,10%)": "2.5", "=TRIMMEAN(A1:B4,70%)": "2.5", @@ -3048,6 +3054,19 @@ func TestCalcCellValue(t *testing.T) { "=TDIST(-1,10,1)": "#NUM!", "=TDIST(1,0,1)": "#NUM!", "=TDIST(1,10,0)": "#NUM!", + // T.INV + "=T.INV()": "T.INV requires 2 arguments", + "=T.INV(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.INV(0.25,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.INV(0,10)": "#NUM!", + "=T.INV(1,10)": "#NUM!", + "=T.INV(0.25,0.5)": "#NUM!", + // T.INV.2T + "=T.INV.2T()": "T.INV.2T requires 2 arguments", + "=T.INV.2T(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.INV.2T(0.25,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.INV.2T(0,10)": "#NUM!", + "=T.INV.2T(0.25,0.5)": "#NUM!", // TRIMMEAN "=TRIMMEAN()": "TRIMMEAN requires 2 arguments", "=TRIMMEAN(A1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", diff --git a/file.go b/file.go index 0135e20eaa..9707a7939c 100644 --- a/file.go +++ b/file.go @@ -63,7 +63,7 @@ func (f *File) Save() error { return f.SaveAs(f.Path) } -// SaveAs provides a function to create or update to an spreadsheet at the +// SaveAs provides a function to create or update to a spreadsheet at the // provided path. func (f *File) SaveAs(name string, opt ...Options) error { if len(name) > MaxFileNameLength { @@ -81,7 +81,7 @@ func (f *File) SaveAs(name string, opt ...Options) error { return ErrWorkbookExt } f.setContentTypePartProjectExtensions(contentType) - file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o600) + file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) if err != nil { return err } diff --git a/lib.go b/lib.go index 4205e085ab..723b976f7a 100644 --- a/lib.go +++ b/lib.go @@ -132,7 +132,7 @@ func (f *File) saveFileList(name string, content []byte) { f.Pkg.Store(name, append([]byte(xml.Header), content...)) } -// Read file content as string in a archive file. +// Read file content as string in an archive file. func readFile(file *zip.File) ([]byte, error) { rc, err := file.Open() if err != nil { @@ -157,8 +157,8 @@ func SplitCellName(cell string) (string, int, error) { if strings.IndexFunc(cell, alpha) == 0 { i := strings.LastIndexFunc(cell, alpha) if i >= 0 && i < len(cell)-1 { - col, rowstr := strings.ReplaceAll(cell[:i+1], "$", ""), cell[i+1:] - if row, err := strconv.Atoi(rowstr); err == nil && row > 0 { + col, rowStr := strings.ReplaceAll(cell[:i+1], "$", ""), cell[i+1:] + if row, err := strconv.Atoi(rowStr); err == nil && row > 0 { return col, row, nil } } @@ -187,7 +187,7 @@ func JoinCellName(col string, row int) (string, error) { } // ColumnNameToNumber provides a function to convert Excel sheet column name -// to int. Column name case insensitive. The function returns an error if +// to int. Column name case-insensitive. The function returns an error if // column name incorrect. // // Example: @@ -248,14 +248,14 @@ func ColumnNumberToName(num int) (string, error) { // excelize.CellNameToCoordinates("Z3") // returns 26, 3, nil // func CellNameToCoordinates(cell string) (int, int, error) { - colname, row, err := SplitCellName(cell) + colName, row, err := SplitCellName(cell) if err != nil { return -1, -1, newCellNameToCoordinatesError(cell, err) } if row > TotalRows { return -1, -1, ErrMaxRows } - col, err := ColumnNameToNumber(colname) + col, err := ColumnNameToNumber(colName) return col, row, err } @@ -277,8 +277,8 @@ func CoordinatesToCellName(col, row int, abs ...bool) (string, error) { sign = "$" } } - colname, err := ColumnNumberToName(col) - return sign + colname + sign + strconv.Itoa(row), err + colName, err := ColumnNumberToName(col) + return sign + colName + sign + strconv.Itoa(row), err } // areaRefToCoordinates provides a function to convert area reference to a @@ -336,6 +336,27 @@ func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) { return firstCell + ":" + lastCell, err } +// getDefinedNameRefTo convert defined name to reference range. +func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string) (refTo string) { + var workbookRefTo, worksheetRefTo string + for _, definedName := range f.GetDefinedName() { + if definedName.Name == definedNameName { + // worksheet scope takes precedence over scope workbook when both definedNames exist + if definedName.Scope == "Workbook" { + workbookRefTo = definedName.RefersTo + } + if definedName.Scope == currentSheet { + worksheetRefTo = definedName.RefersTo + } + } + } + refTo = workbookRefTo + if worksheetRefTo != "" { + refTo = worksheetRefTo + } + return +} + // flatSqref convert reference sequence to cell coordinates list. func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) { var coordinates []int @@ -365,7 +386,7 @@ func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) { return } -// inCoordinates provides a method to check if an coordinate is present in +// inCoordinates provides a method to check if a coordinate is present in // coordinates array, and return the index of its location, otherwise // return -1. func inCoordinates(a [][]int, x []int) int { @@ -391,7 +412,7 @@ func inStrSlice(a []string, x string, caseSensitive bool) int { return -1 } -// inFloat64Slice provides a method to check if an element is present in an +// inFloat64Slice provides a method to check if an element is present in a // float64 array, and return the index of its location, otherwise return -1. func inFloat64Slice(a []float64, x float64) int { for idx, n := range a { @@ -405,7 +426,7 @@ func inFloat64Slice(a []float64, x float64) int { // boolPtr returns a pointer to a bool with the given value. func boolPtr(b bool) *bool { return &b } -// intPtr returns a pointer to a int with the given value. +// intPtr returns a pointer to an int with the given value. func intPtr(i int) *int { return &i } // float64Ptr returns a pointer to a float64 with the given value. @@ -626,7 +647,7 @@ func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte return bytesReplace(contentMarshal, oldXmlns, newXmlns, -1) } -// addNameSpaces provides a function to add a XML attribute by the given +// addNameSpaces provides a function to add an XML attribute by the given // component part path. func (f *File) addNameSpaces(path string, ns xml.Attr) { exist := false @@ -715,7 +736,7 @@ var ( // bstrUnmarshal parses the binary basic string, this will trim escaped string // literal which not permitted in an XML 1.0 document. The basic string -// variant type can store any valid Unicode character. Unicode characters +// variant type can store any valid Unicode character. Unicode's characters // that cannot be directly represented in XML as defined by the XML 1.0 // specification, shall be escaped using the Unicode numerical character // representation escape character format _xHHHH_, where H represents a diff --git a/numfmt.go b/numfmt.go index 685005fbe1..b48c36a1bf 100644 --- a/numfmt.go +++ b/numfmt.go @@ -33,9 +33,9 @@ type languageInfo struct { type numberFormat struct { section []nfp.Section t time.Time - sectionIdx int - isNumberic, hours, seconds bool - number float64 + sectionIdx int + isNumeric, hours, seconds bool + number float64 ap, afterPoint, beforePoint, localCode, result, value, valueSectionType string } @@ -279,7 +279,7 @@ var ( // prepareNumberic split the number into two before and after parts by a // decimal point. func (nf *numberFormat) prepareNumberic(value string) { - if nf.isNumberic, _ = isNumeric(value); !nf.isNumberic { + if nf.isNumeric, _ = isNumeric(value); !nf.isNumeric { return } } @@ -297,7 +297,7 @@ func format(value, numFmt string) string { if section.Type != nf.valueSectionType { continue } - if nf.isNumberic { + if nf.isNumeric { switch section.Type { case nfp.TokenSectionPositive: return nf.positiveHandler() From 26174a2c43755dff794a5d2f48a0d5bdf38e5b1b Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 5 Apr 2022 00:03:46 +0800 Subject: [PATCH 023/213] This closes #1196, fix the compatibility issue and added new formula function ref #65, new formula functions: TINV and TTEST --- calc.go | 141 ++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 61 +++++++++++++++++++++ xmlWorksheet.go | 2 +- 3 files changed, 203 insertions(+), 1 deletion(-) diff --git a/calc.go b/calc.go index c375b71642..b0876a8081 100644 --- a/calc.go +++ b/calc.go @@ -666,12 +666,14 @@ type formulaFuncs struct { // TIMEVALUE // T.INV // T.INV.2T +// TINV // TODAY // TRANSPOSE // TRIM // TRIMMEAN // TRUE // TRUNC +// TTEST // TYPE // UNICHAR // UNICODE @@ -9201,6 +9203,145 @@ func (fn *formulaFuncs) TdotINVdot2T(argsList *list.List) formulaArg { }, degrees.Number/2, degrees.Number)) } +// TINV function calculates the inverse of the two-tailed Student's T +// Distribution, which is a continuous probability distribution that is +// frequently used for testing hypotheses on small sample data sets. The +// syntax of the function is: +// +// TINV(probability,degrees_freedom) +// +func (fn *formulaFuncs) TINV(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "TINV requires 2 arguments") + } + return fn.TdotINVdot2T(argsList) +} + +// tTest calculates the probability associated with the Student's T Test. +func tTest(bTemplin bool, pMat1, pMat2 [][]formulaArg, nC1, nC2, nR1, nR2 int, fT, fF float64) (float64, float64, bool) { + var fCount1, fCount2, fSum1, fSumSqr1, fSum2, fSumSqr2 float64 + var fVal formulaArg + for i := 0; i < nC1; i++ { + for j := 0; j < nR1; j++ { + fVal = pMat1[i][j].ToNumber() + if fVal.Type == ArgNumber { + fSum1 += fVal.Number + fSumSqr1 += fVal.Number * fVal.Number + fCount1++ + } + } + } + for i := 0; i < nC2; i++ { + for j := 0; j < nR2; j++ { + fVal = pMat2[i][j].ToNumber() + if fVal.Type == ArgNumber { + fSum2 += fVal.Number + fSumSqr2 += fVal.Number * fVal.Number + fCount2++ + } + } + } + if fCount1 < 2.0 || fCount2 < 2.0 { + return 0, 0, false + } + if bTemplin { + fS1 := (fSumSqr1 - fSum1*fSum1/fCount1) / (fCount1 - 1) / fCount1 + fS2 := (fSumSqr2 - fSum2*fSum2/fCount2) / (fCount2 - 1) / fCount2 + if fS1+fS2 == 0 { + return 0, 0, false + } + c := fS1 / (fS1 + fS2) + fT = math.Abs(fSum1/fCount1-fSum2/fCount2) / math.Sqrt(fS1+fS2) + fF = 1 / (c*c/(fCount1-1) + (1-c)*(1-c)/(fCount2-1)) + return fT, fF, true + } + fS1 := (fSumSqr1 - fSum1*fSum1/fCount1) / (fCount1 - 1) + fS2 := (fSumSqr2 - fSum2*fSum2/fCount2) / (fCount2 - 1) + fT = math.Abs(fSum1/fCount1-fSum2/fCount2) / math.Sqrt((fCount1-1)*fS1+(fCount2-1)*fS2) * math.Sqrt(fCount1*fCount2*(fCount1+fCount2-2)/(fCount1+fCount2)) + fF = fCount1 + fCount2 - 2 + return fT, fF, true +} + +// tTest is an implementation of the formula function TTEST. +func (fn *formulaFuncs) tTest(pMat1, pMat2 [][]formulaArg, fTails, fTyp float64) formulaArg { + var fT, fF float64 + nC1 := len(pMat1) + nC2 := len(pMat2) + nR1 := len(pMat1[0]) + nR2 := len(pMat2[0]) + ok := true + if fTyp == 1 { + if nC1 != nC2 || nR1 != nR2 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + var fCount, fSum1, fSum2, fSumSqrD float64 + var fVal1, fVal2 formulaArg + for i := 0; i < nC1; i++ { + for j := 0; j < nR1; j++ { + fVal1 = pMat1[i][j].ToNumber() + fVal2 = pMat2[i][j].ToNumber() + if fVal1.Type != ArgNumber || fVal2.Type != ArgNumber { + continue + } + fSum1 += fVal1.Number + fSum2 += fVal2.Number + fSumSqrD += (fVal1.Number - fVal2.Number) * (fVal1.Number - fVal2.Number) + fCount++ + } + } + if fCount < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + fSumD := fSum1 - fSum2 + fDivider := fCount*fSumSqrD - fSumD*fSumD + if fDivider == 0 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + fT = math.Abs(fSumD) * math.Sqrt((fCount-1)/fDivider) + fF = fCount - 1 + } else if fTyp == 2 { + fT, fF, ok = tTest(false, pMat1, pMat2, nC1, nC2, nR1, nR2, fT, fF) + } else { + fT, fF, ok = tTest(true, pMat1, pMat2, nC1, nC2, nR1, nR2, fT, fF) + } + if !ok { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(getTDist(fT, fF, fTails)) +} + +// TTEST function calculates the probability associated with the Student's T +// Test, which is commonly used for identifying whether two data sets are +// likely to have come from the same two underlying populations with the same +// mean. The syntax of the function is: +// +// TTEST(array1,array2,tails,type) +// +func (fn *formulaFuncs) TTEST(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "TTEST requires 4 arguments") + } + var array1, array2, tails, typeArg formulaArg + array1 = argsList.Front().Value.(formulaArg) + array2 = argsList.Front().Next().Value.(formulaArg) + if tails = argsList.Front().Next().Next().Value.(formulaArg).ToNumber(); tails.Type != ArgNumber { + return tails + } + if typeArg = argsList.Back().Value.(formulaArg).ToNumber(); typeArg.Type != ArgNumber { + return typeArg + } + if len(array1.Matrix) == 0 || len(array2.Matrix) == 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if tails.Number != 1 && tails.Number != 2 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if typeArg.Number != 1 && typeArg.Number != 2 && typeArg.Number != 3 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return fn.tTest(array1.Matrix, array2.Matrix, tails.Number, typeArg.Number) +} + // TRIMMEAN function calculates the trimmed mean (or truncated mean) of a // supplied set of values. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 8565038d9c..9b8b226363 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1173,6 +1173,9 @@ func TestCalcCellValue(t *testing.T) { // T.INV.2T "=T.INV.2T(1,10)": "0", "=T.INV.2T(0.5,10)": "0.699812061312432", + // TINV + "=TINV(1,10)": "0", + "=TINV(0.5,10)": "0.699812061312432", // TRIMMEAN "=TRIMMEAN(A1:B4,10%)": "2.5", "=TRIMMEAN(A1:B4,70%)": "2.5", @@ -3067,6 +3070,12 @@ func TestCalcCellValue(t *testing.T) { "=T.INV.2T(0.25,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=T.INV.2T(0,10)": "#NUM!", "=T.INV.2T(0.25,0.5)": "#NUM!", + // TINV + "=TINV()": "TINV requires 2 arguments", + "=TINV(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TINV(0.25,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TINV(0,10)": "#NUM!", + "=TINV(0.25,0.5)": "#NUM!", // TRIMMEAN "=TRIMMEAN()": "TRIMMEAN requires 2 arguments", "=TRIMMEAN(A1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -4888,6 +4897,58 @@ func TestCalcSHEETS(t *testing.T) { } } +func TestCalcTTEST(t *testing.T) { + cellData := [][]interface{}{ + {4, 8, nil, 1, 1}, + {5, 3, nil, 1, 1}, + {2, 7}, + {5, 3}, + {8, 5}, + {9, 2}, + {3, 2}, + {2, 7}, + {3, 9}, + {8, 4}, + {9, 4}, + {5, 7}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=TTEST(A1:A12,B1:B12,1,1)": "0.44907068944428", + "=TTEST(A1:A12,B1:B12,1,2)": "0.436717306029283", + "=TTEST(A1:A12,B1:B12,1,3)": "0.436722015384755", + "=TTEST(A1:A12,B1:B12,2,1)": "0.898141378888559", + "=TTEST(A1:A12,B1:B12,2,2)": "0.873434612058567", + "=TTEST(A1:A12,B1:B12,2,3)": "0.873444030769511", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError := map[string]string{ + "=TTEST()": "TTEST requires 4 arguments", + "=TTEST(\"\",B1:B12,1,1)": "#NUM!", + "=TTEST(A1:A12,\"\",1,1)": "#NUM!", + "=TTEST(A1:A12,B1:B12,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TTEST(A1:A12,B1:B12,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TTEST(A1:A12,B1:B12,0,1)": "#NUM!", + "=TTEST(A1:A12,B1:B12,1,0)": "#NUM!", + "=TTEST(A1:A2,B1:B1,1,1)": "#N/A", + "=TTEST(A13:A14,B13:B14,1,1)": "#NUM!", + "=TTEST(A12:A13,B12:B13,1,1)": "#DIV/0!", + "=TTEST(A13:A14,B13:B14,1,2)": "#NUM!", + "=TTEST(D1:D4,E1:E4,1,3)": "#NUM!", + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.EqualError(t, err, expected, formula) + assert.Equal(t, "", result, formula) + } +} + func TestCalcZTEST(t *testing.T) { f := NewFile() assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{4, 5, 2, 5, 8, 9, 3, 2, 3, 8, 9, 5})) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 13deba56fd..e4d52ecb0b 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -58,9 +58,9 @@ type xlsxWorksheet struct { OleObjects *xlsxInnerXML `xml:"oleObjects"` Controls *xlsxInnerXML `xml:"controls"` WebPublishItems *xlsxInnerXML `xml:"webPublishItems"` + AlternateContent *xlsxAlternateContent `xml:"mc:AlternateContent"` TableParts *xlsxTableParts `xml:"tableParts"` ExtLst *xlsxExtLst `xml:"extLst"` - AlternateContent *xlsxAlternateContent `xml:"mc:AlternateContent"` DecodeAlternateContent *xlsxInnerXML `xml:"http://schemas.openxmlformats.org/markup-compatibility/2006 AlternateContent"` } From 5bf4bce9d41b2f8cd9d24e0d57a0d6868ef9433d Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 6 Apr 2022 00:03:22 +0800 Subject: [PATCH 024/213] ref #65, #1196: fix the compatibility issue and added new formula function - New formula functions: MODE and T.TEST --- calc.go | 61 +++++++++++++++++++++++++++++++++ calc_test.go | 91 ++++++++++++++++++++++++++++++++++++++++---------- numfmt.go | 6 ++-- xmlWorkbook.go | 2 +- 4 files changed, 138 insertions(+), 22 deletions(-) diff --git a/calc.go b/calc.go index b0876a8081..d921c3540a 100644 --- a/calc.go +++ b/calc.go @@ -549,6 +549,7 @@ type formulaFuncs struct { // MINUTE // MIRR // MOD +// MODE // MONTH // MROUND // MULTINOMIAL @@ -673,6 +674,7 @@ type formulaFuncs struct { // TRIMMEAN // TRUE // TRUNC +// T.TEST // TTEST // TYPE // UNICHAR @@ -7974,6 +7976,51 @@ func (fn *formulaFuncs) LOGNORMDIST(argsList *list.List) formulaArg { return fn.NORMSDIST(args) } +// MODE function returns the statistical mode (the most frequently occurring +// value) of a list of supplied numbers. If there are 2 or more most +// frequently occurring values in the supplied data, the function returns the +// lowest of these values The syntax of the function is: +// +// MODE(number1,[number2],...) +// +func (fn *formulaFuncs) MODE(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "MODE requires at least 1 argument") + } + var values []float64 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + cells := arg.Value.(formulaArg) + if cells.Type != ArgMatrix && cells.ToNumber().Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + for _, cell := range cells.ToList() { + if num := cell.ToNumber(); num.Type == ArgNumber { + values = append(values, num.Number) + } + } + } + sort.Float64s(values) + cnt := len(values) + var count, modeCnt int + var mode float64 + for i := 0; i < cnt; i++ { + count = 0 + for j := 0; j < cnt; j++ { + if j != i && values[j] == values[i] { + count++ + } + } + if count > modeCnt { + modeCnt = count + mode = values[i] + } + } + if modeCnt == 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg(mode) +} + // NEGBINOMdotDIST function calculates the probability mass function or the // cumulative distribution function for the Negative Binomial Distribution. // This gives the probability that there will be a given number of failures @@ -9342,6 +9389,20 @@ func (fn *formulaFuncs) TTEST(argsList *list.List) formulaArg { return fn.tTest(array1.Matrix, array2.Matrix, tails.Number, typeArg.Number) } +// TdotTEST function calculates the probability associated with the Student's T +// Test, which is commonly used for identifying whether two data sets are +// likely to have come from the same two underlying populations with the same +// mean. The syntax of the function is: +// +// T.TEST(array1,array2,tails,type) +// +func (fn *formulaFuncs) TdotTEST(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "T.TEST requires 4 arguments") + } + return fn.TTEST(argsList) +} + // TRIMMEAN function calculates the trimmed mean (or truncated mean) of a // supplied set of values. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 9b8b226363..f6499def76 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4865,6 +4865,43 @@ func TestCalcISFORMULA(t *testing.T) { } } +func TestCalcMODE(t *testing.T) { + cellData := [][]interface{}{ + {1, 1}, + {1, 1}, + {2, 2}, + {2, 2}, + {3, 2}, + {3}, + {3}, + {4}, + {4}, + {4}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=MODE(A1:A10)": "3", + "=MODE(B1:B6)": "2", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError := map[string]string{ + "=MODE()": "MODE requires at least 1 argument", + "=MODE(0,\"\")": "#VALUE!", + "=MODE(D1:D3)": "#N/A", + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.EqualError(t, err, expected, formula) + assert.Equal(t, "", result, formula) + } +} + func TestCalcSHEET(t *testing.T) { f := NewFile() f.NewSheet("Sheet2") @@ -4914,12 +4951,18 @@ func TestCalcTTEST(t *testing.T) { } f := prepareCalcData(cellData) formulaList := map[string]string{ - "=TTEST(A1:A12,B1:B12,1,1)": "0.44907068944428", - "=TTEST(A1:A12,B1:B12,1,2)": "0.436717306029283", - "=TTEST(A1:A12,B1:B12,1,3)": "0.436722015384755", - "=TTEST(A1:A12,B1:B12,2,1)": "0.898141378888559", - "=TTEST(A1:A12,B1:B12,2,2)": "0.873434612058567", - "=TTEST(A1:A12,B1:B12,2,3)": "0.873444030769511", + "=TTEST(A1:A12,B1:B12,1,1)": "0.44907068944428", + "=TTEST(A1:A12,B1:B12,1,2)": "0.436717306029283", + "=TTEST(A1:A12,B1:B12,1,3)": "0.436722015384755", + "=TTEST(A1:A12,B1:B12,2,1)": "0.898141378888559", + "=TTEST(A1:A12,B1:B12,2,2)": "0.873434612058567", + "=TTEST(A1:A12,B1:B12,2,3)": "0.873444030769511", + "=T.TEST(A1:A12,B1:B12,1,1)": "0.44907068944428", + "=T.TEST(A1:A12,B1:B12,1,2)": "0.436717306029283", + "=T.TEST(A1:A12,B1:B12,1,3)": "0.436722015384755", + "=T.TEST(A1:A12,B1:B12,2,1)": "0.898141378888559", + "=T.TEST(A1:A12,B1:B12,2,2)": "0.873434612058567", + "=T.TEST(A1:A12,B1:B12,2,3)": "0.873444030769511", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) @@ -4928,18 +4971,30 @@ func TestCalcTTEST(t *testing.T) { assert.Equal(t, expected, result, formula) } calcError := map[string]string{ - "=TTEST()": "TTEST requires 4 arguments", - "=TTEST(\"\",B1:B12,1,1)": "#NUM!", - "=TTEST(A1:A12,\"\",1,1)": "#NUM!", - "=TTEST(A1:A12,B1:B12,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=TTEST(A1:A12,B1:B12,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=TTEST(A1:A12,B1:B12,0,1)": "#NUM!", - "=TTEST(A1:A12,B1:B12,1,0)": "#NUM!", - "=TTEST(A1:A2,B1:B1,1,1)": "#N/A", - "=TTEST(A13:A14,B13:B14,1,1)": "#NUM!", - "=TTEST(A12:A13,B12:B13,1,1)": "#DIV/0!", - "=TTEST(A13:A14,B13:B14,1,2)": "#NUM!", - "=TTEST(D1:D4,E1:E4,1,3)": "#NUM!", + "=TTEST()": "TTEST requires 4 arguments", + "=TTEST(\"\",B1:B12,1,1)": "#NUM!", + "=TTEST(A1:A12,\"\",1,1)": "#NUM!", + "=TTEST(A1:A12,B1:B12,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TTEST(A1:A12,B1:B12,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TTEST(A1:A12,B1:B12,0,1)": "#NUM!", + "=TTEST(A1:A12,B1:B12,1,0)": "#NUM!", + "=TTEST(A1:A2,B1:B1,1,1)": "#N/A", + "=TTEST(A13:A14,B13:B14,1,1)": "#NUM!", + "=TTEST(A12:A13,B12:B13,1,1)": "#DIV/0!", + "=TTEST(A13:A14,B13:B14,1,2)": "#NUM!", + "=TTEST(D1:D4,E1:E4,1,3)": "#NUM!", + "=T.TEST()": "T.TEST requires 4 arguments", + "=T.TEST(\"\",B1:B12,1,1)": "#NUM!", + "=T.TEST(A1:A12,\"\",1,1)": "#NUM!", + "=T.TEST(A1:A12,B1:B12,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.TEST(A1:A12,B1:B12,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.TEST(A1:A12,B1:B12,0,1)": "#NUM!", + "=T.TEST(A1:A12,B1:B12,1,0)": "#NUM!", + "=T.TEST(A1:A2,B1:B1,1,1)": "#N/A", + "=T.TEST(A13:A14,B13:B14,1,1)": "#NUM!", + "=T.TEST(A12:A13,B12:B13,1,1)": "#DIV/0!", + "=T.TEST(A13:A14,B13:B14,1,2)": "#NUM!", + "=T.TEST(D1:D4,E1:E4,1,3)": "#NUM!", } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) diff --git a/numfmt.go b/numfmt.go index b48c36a1bf..6cb7fc7493 100644 --- a/numfmt.go +++ b/numfmt.go @@ -33,9 +33,9 @@ type languageInfo struct { type numberFormat struct { section []nfp.Section t time.Time - sectionIdx int - isNumeric, hours, seconds bool - number float64 + sectionIdx int + isNumeric, hours, seconds bool + number float64 ap, afterPoint, beforePoint, localCode, result, value, valueSectionType string } diff --git a/xmlWorkbook.go b/xmlWorkbook.go index a500a34d4c..a0fce15f2e 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -40,9 +40,9 @@ type xlsxWorkbook struct { FileVersion *xlsxFileVersion `xml:"fileVersion"` FileSharing *xlsxExtLst `xml:"fileSharing"` WorkbookPr *xlsxWorkbookPr `xml:"workbookPr"` - WorkbookProtection *xlsxWorkbookProtection `xml:"workbookProtection"` AlternateContent *xlsxAlternateContent `xml:"mc:AlternateContent"` DecodeAlternateContent *xlsxInnerXML `xml:"http://schemas.openxmlformats.org/markup-compatibility/2006 AlternateContent"` + WorkbookProtection *xlsxWorkbookProtection `xml:"workbookProtection"` BookViews *xlsxBookViews `xml:"bookViews"` Sheets xlsxSheets `xml:"sheets"` FunctionGroups *xlsxExtLst `xml:"functionGroups"` From 9b8f1a15e1b75f56d9305b49212ee34ec085943f Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 7 Apr 2022 08:16:55 +0800 Subject: [PATCH 025/213] ref #65, new formula functions: MODE.MULT and MODE.SNGL --- calc.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 19 +++++++++++----- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/calc.go b/calc.go index d921c3540a..20d5b5e707 100644 --- a/calc.go +++ b/calc.go @@ -550,6 +550,8 @@ type formulaFuncs struct { // MIRR // MOD // MODE +// MODE.MULT +// MODE.SNGL // MONTH // MROUND // MULTINOMIAL @@ -8021,6 +8023,67 @@ func (fn *formulaFuncs) MODE(argsList *list.List) formulaArg { return newNumberFormulaArg(mode) } +// MODEdotMULT function returns a vertical array of the statistical modes +// (the most frequently occurring values) within a list of supplied numbers. +// The syntax of the function is: +// +// MODE.MULT(number1,[number2],...) +// +func (fn *formulaFuncs) MODEdotMULT(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "MODE.MULT requires at least 1 argument") + } + var values []float64 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + cells := arg.Value.(formulaArg) + if cells.Type != ArgMatrix && cells.ToNumber().Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + for _, cell := range cells.ToList() { + if num := cell.ToNumber(); num.Type == ArgNumber { + values = append(values, num.Number) + } + } + } + sort.Float64s(values) + cnt := len(values) + var count, modeCnt int + var mtx [][]formulaArg + for i := 0; i < cnt; i++ { + count = 0 + for j := i + 1; j < cnt; j++ { + if values[i] == values[j] { + count++ + } + } + if count > modeCnt { + modeCnt = count + mtx = [][]formulaArg{} + mtx = append(mtx, []formulaArg{newNumberFormulaArg(values[i])}) + } else if count == modeCnt { + mtx = append(mtx, []formulaArg{newNumberFormulaArg(values[i])}) + } + } + if modeCnt == 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newMatrixFormulaArg(mtx) +} + +// MODEdotSNGL function returns the statistical mode (the most frequently +// occurring value) within a list of supplied numbers. If there are 2 or more +// most frequently occurring values in the supplied data, the function returns +// the lowest of these values. The syntax of the function is: +// +// MODE.SNGL(number1,[number2],...) +// +func (fn *formulaFuncs) MODEdotSNGL(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "MODE.SNGL requires at least 1 argument") + } + return fn.MODE(argsList) +} + // NEGBINOMdotDIST function calculates the probability mass function or the // cumulative distribution function for the Negative Binomial Distribution. // This gives the probability that there will be a given number of failures diff --git a/calc_test.go b/calc_test.go index f6499def76..689fd92b6b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4880,8 +4880,11 @@ func TestCalcMODE(t *testing.T) { } f := prepareCalcData(cellData) formulaList := map[string]string{ - "=MODE(A1:A10)": "3", - "=MODE(B1:B6)": "2", + "=MODE(A1:A10)": "3", + "=MODE(B1:B6)": "2", + "=MODE.MULT(A1:A10)": "", + "=MODE.SNGL(A1:A10)": "3", + "=MODE.SNGL(B1:B6)": "2", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) @@ -4890,9 +4893,15 @@ func TestCalcMODE(t *testing.T) { assert.Equal(t, expected, result, formula) } calcError := map[string]string{ - "=MODE()": "MODE requires at least 1 argument", - "=MODE(0,\"\")": "#VALUE!", - "=MODE(D1:D3)": "#N/A", + "=MODE()": "MODE requires at least 1 argument", + "=MODE(0,\"\")": "#VALUE!", + "=MODE(D1:D3)": "#N/A", + "=MODE.MULT()": "MODE.MULT requires at least 1 argument", + "=MODE.MULT(0,\"\")": "#VALUE!", + "=MODE.MULT(D1:D3)": "#N/A", + "=MODE.SNGL()": "MODE.SNGL requires at least 1 argument", + "=MODE.SNGL(0,\"\")": "#VALUE!", + "=MODE.SNGL(D1:D3)": "#N/A", } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) From c1940c2a1ebd66519bb85abaa2fd7985f0430985 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 11 Apr 2022 00:04:00 +0800 Subject: [PATCH 026/213] This includes new formula functions support, dependencies upgrade, and bug fix - Fix page setup fields parsing issue - Go Modules dependencies upgrade - Ref #65, CONFIDENCE.T and PHI - Ref #1198, Fix the issue that the chart axis maximum and minimum didn't work when the value is 0 --- calc.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++--- calc_test.go | 20 +++++++++++++++++ drawing.go | 36 ++++++++++++++--------------- go.mod | 8 +++---- go.sum | 16 ++++++------- xmlChart.go | 34 ++++++++++++++-------------- xmlWorksheet.go | 4 ++-- 7 files changed, 126 insertions(+), 52 deletions(-) diff --git a/calc.go b/calc.go index 20d5b5e707..57b2cda837 100644 --- a/calc.go +++ b/calc.go @@ -374,6 +374,7 @@ type formulaFuncs struct { // CONCATENATE // CONFIDENCE // CONFIDENCE.NORM +// CONFIDENCE.T // CORREL // COS // COSH @@ -588,6 +589,7 @@ type formulaFuncs struct { // PERCENTRANK // PERMUT // PERMUTATIONA +// PHI // PI // PMT // POISSON.DIST @@ -6805,7 +6807,7 @@ func (fn *formulaFuncs) CONFIDENCE(argsList *list.List) formulaArg { // confidence value that can be used to construct the confidence interval for // a population mean, for a supplied probability and sample size. It is // assumed that the standard deviation of the population is known. The syntax -// of the Confidence.Norm function is: +// of the function is: // // CONFIDENCE.NORM(alpha,standard_dev,size) // @@ -6813,6 +6815,42 @@ func (fn *formulaFuncs) CONFIDENCEdotNORM(argsList *list.List) formulaArg { return fn.confidence("CONFIDENCE.NORM", argsList) } +// CONFIDENCEdotT function uses a Student's T-Distribution to calculate a +// confidence value that can be used to construct the confidence interval for +// a population mean, for a supplied probablity and supplied sample size. It +// is assumed that the standard deviation of the population is known. The +// syntax of the function is: +// +// CONFIDENCE.T(alpha,standard_dev,size) +// +func (fn *formulaFuncs) CONFIDENCEdotT(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "CONFIDENCE.T requires 3 arguments") + } + var alpha, standardDev, size formulaArg + if alpha = argsList.Front().Value.(formulaArg).ToNumber(); alpha.Type != ArgNumber { + return alpha + } + if standardDev = argsList.Front().Next().Value.(formulaArg).ToNumber(); standardDev.Type != ArgNumber { + return standardDev + } + if size = argsList.Back().Value.(formulaArg).ToNumber(); size.Type != ArgNumber { + return size + } + if alpha.Number <= 0 || alpha.Number >= 1 || standardDev.Number <= 0 || size.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if size.Number == 1 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + return newNumberFormulaArg(standardDev.Number * calcIterateInverse(calcInverseIterator{ + name: "CONFIDENCE.T", + fp: alpha.Number, + fDF: size.Number - 1, + nT: 2, + }, size.Number/2, size.Number) / math.Sqrt(size.Number)) +} + // COVAR function calculates the covariance of two supplied sets of values. The // syntax of the function is: // @@ -8891,6 +8929,22 @@ func (fn *formulaFuncs) PERMUTATIONA(argsList *list.List) formulaArg { return newNumberFormulaArg(math.Pow(num, numChosen)) } +// PHI function returns the value of the density function for a standard normal +// distribution for a supplied number. The syntax of the function is: +// +// PHI(x) +// +func (fn *formulaFuncs) PHI(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "PHI requires 1 argument") + } + x := argsList.Front().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + return newNumberFormulaArg(0.39894228040143268 * math.Exp(-(x.Number*x.Number)/2)) +} + // QUARTILE function returns a requested quartile of a supplied range of // values. The syntax of the function is: // @@ -13122,7 +13176,7 @@ func validateFrequency(freq float64) bool { return freq == 1 || freq == 2 || freq == 4 } -// ACCRINT function returns the accrued interest for a security that pays +// ACCRINT function returns the accrued interest in a security that pays // periodic interest. The syntax of the function is: // // ACCRINT(issue,first_interest,settlement,rate,par,frequency,[basis],[calc_method]) @@ -13166,7 +13220,7 @@ func (fn *formulaFuncs) ACCRINT(argsList *list.List) formulaArg { return newNumberFormulaArg(par.Number * rate.Number * frac1.Number) } -// ACCRINTM function returns the accrued interest for a security that pays +// ACCRINTM function returns the accrued interest in a security that pays // interest at maturity. The syntax of the function is: // // ACCRINTM(issue,settlement,rate,[par],[basis]) diff --git a/calc_test.go b/calc_test.go index 689fd92b6b..c553d1294d 100644 --- a/calc_test.go +++ b/calc_test.go @@ -879,6 +879,8 @@ func TestCalcCellValue(t *testing.T) { "=CONFIDENCE(0.05,0.07,100)": "0.0137197479028414", // CONFIDENCE.NORM "=CONFIDENCE.NORM(0.05,0.07,100)": "0.0137197479028414", + // CONFIDENCE.T + "=CONFIDENCE.T(0.05,0.07,100)": "0.0138895186611049", // CORREL "=CORREL(A1:A5,B1:B5)": "1", // COUNT @@ -1122,6 +1124,11 @@ func TestCalcCellValue(t *testing.T) { // PERMUTATIONA "=PERMUTATIONA(6,6)": "46656", "=PERMUTATIONA(7,6)": "117649", + // PHI + "=PHI(-1.5)": "0.129517595665892", + "=PHI(0)": "0.398942280401433", + "=PHI(0.1)": "0.396952547477012", + "=PHI(1)": "0.241970724519143", // QUARTILE "=QUARTILE(A1:A4,2)": "1.5", // QUARTILE.EXC @@ -2643,6 +2650,16 @@ func TestCalcCellValue(t *testing.T) { "=CORREL()": "CORREL requires 2 arguments", "=CORREL(A1:A3,B1:B5)": "#N/A", "=CORREL(A1:A1,B1:B1)": "#DIV/0!", + // CONFIDENCE.T + "=CONFIDENCE.T()": "CONFIDENCE.T requires 3 arguments", + "=CONFIDENCE.T(\"\",0.07,100)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CONFIDENCE.T(0.05,\"\",100)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CONFIDENCE.T(0.05,0.07,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CONFIDENCE.T(0,0.07,100)": "#NUM!", + "=CONFIDENCE.T(1,0.07,100)": "#NUM!", + "=CONFIDENCE.T(0.05,0,100)": "#NUM!", + "=CONFIDENCE.T(0.05,0.07,0)": "#NUM!", + "=CONFIDENCE.T(0.05,0.07,1)": "#DIV/0!", // COUNTBLANK "=COUNTBLANK()": "COUNTBLANK requires 1 argument", "=COUNTBLANK(1,2)": "COUNTBLANK requires 1 argument", @@ -2985,6 +3002,9 @@ func TestCalcCellValue(t *testing.T) { "=PERMUTATIONA(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=PERMUTATIONA(-1,0)": "#N/A", "=PERMUTATIONA(0,-1)": "#N/A", + // PHI + "=PHI()": "PHI requires 1 argument", + "=PHI(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // QUARTILE "=QUARTILE()": "QUARTILE requires 2 arguments", "=QUARTILE(A1:A4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", diff --git a/drawing.go b/drawing.go index 1daa912ae4..d0e9135ba2 100644 --- a/drawing.go +++ b/drawing.go @@ -957,14 +957,14 @@ func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { // drawPlotAreaCatAx provides a function to draw the c:catAx element. func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { - min := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Minimum)} - max := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Maximum)} - if formatSet.XAxis.Minimum == 0 { - min = nil - } - if formatSet.XAxis.Maximum == 0 { + max := &attrValFloat{Val: formatSet.XAxis.Maximum} + min := &attrValFloat{Val: formatSet.XAxis.Minimum} + if formatSet.XAxis.Maximum == nil { max = nil } + if formatSet.XAxis.Minimum == nil { + min = nil + } axs := []*cAxs{ { AxID: &attrValInt{Val: intPtr(754001152)}, @@ -1006,14 +1006,14 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { // drawPlotAreaValAx provides a function to draw the c:valAx element. func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { - min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)} - max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)} - if formatSet.YAxis.Minimum == 0 { - min = nil - } - if formatSet.YAxis.Maximum == 0 { + max := &attrValFloat{Val: formatSet.YAxis.Maximum} + min := &attrValFloat{Val: formatSet.YAxis.Minimum} + if formatSet.YAxis.Maximum == nil { max = nil } + if formatSet.YAxis.Minimum == nil { + min = nil + } var logBase *attrValFloat if formatSet.YAxis.LogBase >= 2 && formatSet.YAxis.LogBase <= 1000 { logBase = &attrValFloat{Val: float64Ptr(formatSet.YAxis.LogBase)} @@ -1060,14 +1060,14 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { // drawPlotAreaSerAx provides a function to draw the c:serAx element. func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs { - min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)} - max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)} - if formatSet.YAxis.Minimum == 0 { - min = nil - } - if formatSet.YAxis.Maximum == 0 { + max := &attrValFloat{Val: formatSet.YAxis.Maximum} + min := &attrValFloat{Val: formatSet.YAxis.Minimum} + if formatSet.YAxis.Maximum == nil { max = nil } + if formatSet.YAxis.Minimum == nil { + min = nil + } return []*cAxs{ { AxID: &attrValInt{Val: intPtr(832256642)}, diff --git a/go.mod b/go.mod index 16874b2189..116b7a1725 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,10 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.7.0 - github.com/xuri/efp v0.0.0-20220216053911-6d8731f62184 - github.com/xuri/nfp v0.0.0-20220215121256-71f1502108b5 - golang.org/x/crypto v0.0.0-20220214200702-86341886e292 + github.com/xuri/efp v0.0.0-20220407160117-ad0f7a785be8 + github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 + golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 golang.org/x/image v0.0.0-20211028202545-6944b10bf410 - golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd + golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 golang.org/x/text v0.3.7 ) diff --git a/go.sum b/go.sum index a4051d4dfd..8ca6233f05 100644 --- a/go.sum +++ b/go.sum @@ -11,17 +11,17 @@ github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTK github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/xuri/efp v0.0.0-20220216053911-6d8731f62184 h1:9nchVQT/GVLRvOnXzx+wUvSublH/jG/ANV4MxBnGhUA= -github.com/xuri/efp v0.0.0-20220216053911-6d8731f62184/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/nfp v0.0.0-20220215121256-71f1502108b5 h1:Pg6lKJe2FUZTalbUygJxgW1ke2re9lY3YW5TKb+Pxe4= -github.com/xuri/nfp v0.0.0-20220215121256-71f1502108b5/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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/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-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM= +golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/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-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 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= diff --git a/xmlChart.go b/xmlChart.go index 45f1ce6dff..b6ee3cd81a 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -520,23 +520,23 @@ type cPageMargins struct { // formatChartAxis directly maps the format settings of the chart axis. type formatChartAxis struct { - None bool `json:"none"` - Crossing string `json:"crossing"` - MajorGridlines bool `json:"major_grid_lines"` - MinorGridlines bool `json:"minor_grid_lines"` - MajorTickMark string `json:"major_tick_mark"` - MinorTickMark string `json:"minor_tick_mark"` - MinorUnitType string `json:"minor_unit_type"` - MajorUnit float64 `json:"major_unit"` - MajorUnitType string `json:"major_unit_type"` - TickLabelSkip int `json:"tick_label_skip"` - DisplayUnits string `json:"display_units"` - DisplayUnitsVisible bool `json:"display_units_visible"` - DateAxis bool `json:"date_axis"` - ReverseOrder bool `json:"reverse_order"` - Maximum float64 `json:"maximum"` - Minimum float64 `json:"minimum"` - NumFormat string `json:"num_format"` + None bool `json:"none"` + Crossing string `json:"crossing"` + MajorGridlines bool `json:"major_grid_lines"` + MinorGridlines bool `json:"minor_grid_lines"` + MajorTickMark string `json:"major_tick_mark"` + MinorTickMark string `json:"minor_tick_mark"` + MinorUnitType string `json:"minor_unit_type"` + MajorUnit float64 `json:"major_unit"` + MajorUnitType string `json:"major_unit_type"` + TickLabelSkip int `json:"tick_label_skip"` + DisplayUnits string `json:"display_units"` + DisplayUnitsVisible bool `json:"display_units_visible"` + DateAxis bool `json:"date_axis"` + ReverseOrder bool `json:"reverse_order"` + Maximum *float64 `json:"maximum"` + Minimum *float64 `json:"minimum"` + NumFormat string `json:"num_format"` NumFont struct { Color string `json:"color"` Bold bool `json:"bold"` diff --git a/xmlWorksheet.go b/xmlWorksheet.go index e4d52ecb0b..eb855c5397 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -116,7 +116,7 @@ type xlsxPageSetUp struct { FirstPageNumber string `xml:"firstPageNumber,attr,omitempty"` FitToHeight *int `xml:"fitToHeight,attr"` FitToWidth *int `xml:"fitToWidth,attr"` - HorizontalDPI float64 `xml:"horizontalDpi,attr,omitempty"` + HorizontalDPI string `xml:"horizontalDpi,attr,omitempty"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` Orientation string `xml:"orientation,attr,omitempty"` PageOrder string `xml:"pageOrder,attr,omitempty"` @@ -126,7 +126,7 @@ type xlsxPageSetUp struct { Scale int `xml:"scale,attr,omitempty"` UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` - VerticalDPI float64 `xml:"verticalDpi,attr,omitempty"` + VerticalDPI string `xml:"verticalDpi,attr,omitempty"` } // xlsxPrintOptions directly maps the printOptions element in the namespace From 396cf99d45b0b7f10acd685fab44f34e5c7ee464 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 12 Apr 2022 08:18:09 +0800 Subject: [PATCH 027/213] ref #65, new formula functions: COVARIANCE.S and STDEVPA --- calc.go | 60 ++++++++++++++++++++++++++++++++++++++++------------ calc_test.go | 19 +++++++++++++---- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/calc.go b/calc.go index 57b2cda837..0931012e63 100644 --- a/calc.go +++ b/calc.go @@ -393,6 +393,7 @@ type formulaFuncs struct { // COUPPCD // COVAR // COVARIANCE.P +// COVARIANCE.S // CRITBINOM // CSC // CSCH @@ -645,6 +646,7 @@ type formulaFuncs struct { // STDEV.S // STDEVA // STDEVP +// STDEVPA // SUBSTITUTE // SUM // SUMIF @@ -6851,14 +6853,11 @@ func (fn *formulaFuncs) CONFIDENCEdotT(argsList *list.List) formulaArg { }, size.Number/2, size.Number) / math.Sqrt(size.Number)) } -// COVAR function calculates the covariance of two supplied sets of values. The -// syntax of the function is: -// -// COVAR(array1,array2) -// -func (fn *formulaFuncs) COVAR(argsList *list.List) formulaArg { +// covar is an implementation of the formula functions COVAR, COVARIANCE.P and +// COVARIANCE.S. +func (fn *formulaFuncs) covar(name string, argsList *list.List) formulaArg { if argsList.Len() != 2 { - return newErrorFormulaArg(formulaErrorVALUE, "COVAR requires 2 arguments") + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name)) } array1 := argsList.Front().Value.(formulaArg) array2 := argsList.Back().Value.(formulaArg) @@ -6881,19 +6880,37 @@ func (fn *formulaFuncs) COVAR(argsList *list.List) formulaArg { } result += (arg1.Number - mean1.Number) * (arg2.Number - mean2.Number) } + if name == "COVARIANCE.S" { + return newNumberFormulaArg(result / float64(n-skip-1)) + } return newNumberFormulaArg(result / float64(n-skip)) } +// COVAR function calculates the covariance of two supplied sets of values. The +// syntax of the function is: +// +// COVAR(array1,array2) +// +func (fn *formulaFuncs) COVAR(argsList *list.List) formulaArg { + return fn.covar("COVAR", argsList) +} + // COVARIANCEdotP function calculates the population covariance of two supplied // sets of values. The syntax of the function is: // // COVARIANCE.P(array1,array2) // func (fn *formulaFuncs) COVARIANCEdotP(argsList *list.List) formulaArg { - if argsList.Len() != 2 { - return newErrorFormulaArg(formulaErrorVALUE, "COVARIANCE.P requires 2 arguments") - } - return fn.COVAR(argsList) + return fn.covar("COVARIANCE.P", argsList) +} + +// COVARIANCEdotS function calculates the sample covariance of two supplied +// sets of values. The syntax of the function is: +// +// COVARIANCE.S(array1,array2) +// +func (fn *formulaFuncs) COVARIANCEdotS(argsList *list.List) formulaArg { + return fn.covar("COVARIANCE.S", argsList) } // calcStringCountSum is part of the implementation countSum. @@ -9131,12 +9148,17 @@ func (fn *formulaFuncs) STANDARDIZE(argsList *list.List) formulaArg { return newNumberFormulaArg((x.Number - mean.Number) / stdDev.Number) } -// stdevp is an implementation of the formula functions STDEVP and STDEV.P. +// stdevp is an implementation of the formula functions STDEVP, STDEV.P and +// STDEVPA. func (fn *formulaFuncs) stdevp(name string, argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name)) } - varp := fn.VARP(argsList) + fnName := "VARP" + if name == "STDEVPA" { + fnName = "VARPA" + } + varp := fn.vars(fnName, argsList) if varp.Type != ArgNumber { return varp } @@ -9161,6 +9183,15 @@ func (fn *formulaFuncs) STDEVdotP(argsList *list.List) formulaArg { return fn.stdevp("STDEV.P", argsList) } +// STDEVPA function calculates the standard deviation of a supplied set of +// values. The syntax of the function is: +// +// STDEVPA(number1,[number2],...) +// +func (fn *formulaFuncs) STDEVPA(argsList *list.List) formulaArg { + return fn.stdevp("STDEVPA", argsList) +} + // getTDist is an implementation for the beta distribution probability density // function. func getTDist(T, fDF, nType float64) float64 { @@ -9576,6 +9607,9 @@ func (fn *formulaFuncs) vars(name string, argsList *list.List) formulaArg { } for arg := argsList.Front(); arg != nil; arg = arg.Next() { for _, token := range arg.Value.(formulaArg).ToList() { + if token.Value() == "" { + continue + } num := token.ToNumber() if token.Value() != "TRUE" && num.Type == ArgNumber { summerA += num.Number * num.Number diff --git a/calc_test.go b/calc_test.go index c553d1294d..98d3d45881 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1162,6 +1162,10 @@ func TestCalcCellValue(t *testing.T) { "=STDEVP(A1:B2,6,-1)": "2.40947204913349", // STDEV.P "=STDEV.P(A1:B2,6,-1)": "2.40947204913349", + // STDEVPA + "=STDEVPA(1,3,5,2)": "1.4790199457749", + "=STDEVPA(1,3,5,2,1,0)": "1.63299316185545", + "=STDEVPA(1,3,5,2,TRUE,\"text\")": "1.63299316185545", // T.DIST "=T.DIST(1,10,TRUE)": "0.82955343384897", "=T.DIST(-1,10,TRUE)": "0.17044656615103", @@ -1190,8 +1194,8 @@ func TestCalcCellValue(t *testing.T) { "=VAR(1,3,5,0,C1)": "4.91666666666667", "=VAR(1,3,5,0,C1,TRUE)": "4", // VARA - "=VARA(1,3,5,0,C1)": "4.7", - "=VARA(1,3,5,0,C1,TRUE)": "3.86666666666667", + "=VARA(1,3,5,0,C1)": "4.91666666666667", + "=VARA(1,3,5,0,C1,TRUE)": "4", // VARP "=VARP(A1:A5)": "1.25", "=VARP(1,3,5,0,C1,TRUE)": "3.2", @@ -1201,8 +1205,8 @@ func TestCalcCellValue(t *testing.T) { "=VAR.S(1,3,5,0,C1)": "4.91666666666667", "=VAR.S(1,3,5,0,C1,TRUE)": "4", // VARPA - "=VARPA(1,3,5,0,C1)": "3.76", - "=VARPA(1,3,5,0,C1,TRUE)": "3.22222222222222", + "=VARPA(1,3,5,0,C1)": "3.6875", + "=VARPA(1,3,5,0,C1,TRUE)": "3.2", // WEIBULL "=WEIBULL(1,3,1,FALSE)": "1.10363832351433", "=WEIBULL(2,5,1.5,TRUE)": "0.985212776817482", @@ -3050,6 +3054,9 @@ func TestCalcCellValue(t *testing.T) { // STDEV.P "=STDEV.P()": "STDEV.P requires at least 1 argument", "=STDEV.P(\"\")": "#DIV/0!", + // STDEVPA + "=STDEVPA()": "STDEVPA requires at least 1 argument", + "=STDEVPA(\"\")": "#DIV/0!", // T.DIST "=T.DIST()": "T.DIST requires 3 arguments", "=T.DIST(\"\",10,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -4361,6 +4368,8 @@ func TestCalcCOVAR(t *testing.T) { "=COVAR(A2:A9,B2:B9)": "16.633125", "=COVARIANCE.P(A1:A9,B1:B9)": "16.633125", "=COVARIANCE.P(A2:A9,B2:B9)": "16.633125", + "=COVARIANCE.S(A1:A9,B1:B9)": "19.0092857142857", + "=COVARIANCE.S(A2:A9,B2:B9)": "19.0092857142857", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) @@ -4373,6 +4382,8 @@ func TestCalcCOVAR(t *testing.T) { "=COVAR(A2:A9,B3:B3)": "#N/A", "=COVARIANCE.P()": "COVARIANCE.P requires 2 arguments", "=COVARIANCE.P(A2:A9,B3:B3)": "#N/A", + "=COVARIANCE.S()": "COVARIANCE.S requires 2 arguments", + "=COVARIANCE.S(A2:A9,B3:B3)": "#N/A", } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) From c0d341706d7e6d568bb94444d58799f001a97c3f Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 14 Apr 2022 00:08:26 +0800 Subject: [PATCH 028/213] ref #65, new formula functions: MINVERSE and MMULT --- calc.go | 170 +++++++++++++++++++++++++++++++++++++++++++++------ calc_test.go | 16 ++++- 2 files changed, 166 insertions(+), 20 deletions(-) diff --git a/calc.go b/calc.go index 0931012e63..907e90ef03 100644 --- a/calc.go +++ b/calc.go @@ -549,7 +549,9 @@ type formulaFuncs struct { // MINA // MINIFS // MINUTE +// MINVERSE // MIRR +// MMULT // MOD // MODE // MODE.MULT @@ -4042,37 +4044,167 @@ func det(sqMtx [][]float64) float64 { return res } +// newNumberMatrix converts a formula arguments matrix to a number matrix. +func newNumberMatrix(arg formulaArg, phalanx bool) (numMtx [][]float64, ele formulaArg) { + rows := len(arg.Matrix) + for r, row := range arg.Matrix { + if phalanx && len(row) != rows { + ele = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + return + } + numMtx = append(numMtx, make([]float64, len(row))) + for c, cell := range row { + if ele = cell.ToNumber(); ele.Type != ArgNumber { + return + } + numMtx[r][c] = ele.Number + } + } + return +} + +// newFormulaArgMatrix converts the number formula arguments matrix to a +// formula arguments matrix. +func newFormulaArgMatrix(numMtx [][]float64) (arg [][]formulaArg) { + for r, row := range numMtx { + arg = append(arg, make([]formulaArg, len(row))) + for c, cell := range row { + arg[r][c] = newNumberFormulaArg(cell) + } + } + return +} + // MDETERM calculates the determinant of a square matrix. The // syntax of the function is: // // MDETERM(array) // func (fn *formulaFuncs) MDETERM(argsList *list.List) (result formulaArg) { - var ( - num float64 - numMtx [][]float64 - err error - strMtx [][]formulaArg - ) if argsList.Len() < 1 { - return newErrorFormulaArg(formulaErrorVALUE, "MDETERM requires at least 1 argument") + return newErrorFormulaArg(formulaErrorVALUE, "MDETERM requires 1 argument") } - strMtx = argsList.Front().Value.(formulaArg).Matrix - rows := len(strMtx) - for _, row := range argsList.Front().Value.(formulaArg).Matrix { - if len(row) != rows { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + numMtx, errArg := newNumberMatrix(argsList.Front().Value.(formulaArg), true) + if errArg.Type == ArgError { + return errArg + } + return newNumberFormulaArg(det(numMtx)) +} + +// cofactorMatrix returns the matrix A of cofactors. +func cofactorMatrix(i, j int, A [][]float64) float64 { + N, sign := len(A), -1.0 + if (i+j)%2 == 0 { + sign = 1 + } + var B [][]float64 + for _, row := range A { + B = append(B, row) + } + for m := 0; m < N; m++ { + for n := j + 1; n < N; n++ { + B[m][n-1] = B[m][n] } - var numRow []float64 - for _, ele := range row { - if num, err = strconv.ParseFloat(ele.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + B[m] = B[m][:len(B[m])-1] + } + for k := i + 1; k < N; k++ { + B[k-1] = B[k] + } + B = B[:len(B)-1] + return sign * det(B) +} + +// adjugateMatrix returns transpose of the cofactor matrix A with Cramer's +// rule. +func adjugateMatrix(A [][]float64) (adjA [][]float64) { + N := len(A) + var B [][]float64 + for i := 0; i < N; i++ { + adjA = append(adjA, make([]float64, N)) + for j := 0; j < N; j++ { + for m := 0; m < N; m++ { + for n := 0; n < N; n++ { + for x := len(B); x <= m; x++ { + B = append(B, []float64{}) + } + for k := len(B[m]); k <= n; k++ { + B[m] = append(B[m], 0) + } + B[m][n] = A[m][n] + } } - numRow = append(numRow, num) + adjA[i][j] = cofactorMatrix(j, i, B) } - numMtx = append(numMtx, numRow) } - return newNumberFormulaArg(det(numMtx)) + return +} + +// MINVERSE function calculates the inverse of a square matrix. The syntax of +// the function is: +// +// MINVERSE(array) +// +func (fn *formulaFuncs) MINVERSE(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "MINVERSE requires 1 argument") + } + numMtx, errArg := newNumberMatrix(argsList.Front().Value.(formulaArg), true) + if errArg.Type == ArgError { + return errArg + } + if detM := det(numMtx); detM != 0 { + datM, invertM := 1/detM, adjugateMatrix(numMtx) + for i := 0; i < len(invertM); i++ { + for j := 0; j < len(invertM[i]); j++ { + invertM[i][j] *= datM + } + } + return newMatrixFormulaArg(newFormulaArgMatrix(invertM)) + } + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) +} + +// MMULT function calculates the matrix product of two arrays +// (representing matrices). The syntax of the function is: +// +// MMULT(array1,array2) +// +func (fn *formulaFuncs) MMULT(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "MMULT requires 2 argument") + } + numMtx1, errArg1 := newNumberMatrix(argsList.Front().Value.(formulaArg), false) + if errArg1.Type == ArgError { + return errArg1 + } + numMtx2, errArg2 := newNumberMatrix(argsList.Back().Value.(formulaArg), false) + if errArg2.Type == ArgError { + return errArg2 + } + array2Rows, array2Cols := len(numMtx2), len(numMtx2[0]) + if len(numMtx1[0]) != array2Rows { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + var numMtx [][]float64 + var row1, row []float64 + var sum float64 + for i := 0; i < len(numMtx1); i++ { + numMtx = append(numMtx, []float64{}) + row = []float64{} + row1 = numMtx1[i] + for j := 0; j < array2Cols; j++ { + sum = 0 + for k := 0; k < array2Rows; k++ { + sum += row1[k] * numMtx2[k][j] + } + for l := len(row); l <= j; l++ { + row = append(row, 0) + } + row[j] = sum + numMtx[i] = row + } + } + return newMatrixFormulaArg(newFormulaArgMatrix(numMtx)) } // MOD function returns the remainder of a division between two supplied diff --git a/calc_test.go b/calc_test.go index 98d3d45881..52bc06187d 100644 --- a/calc_test.go +++ b/calc_test.go @@ -571,6 +571,10 @@ func TestCalcCellValue(t *testing.T) { "=IMPRODUCT(\"1-i\",\"5+10i\",2)": "30+10i", "=IMPRODUCT(COMPLEX(5,2),COMPLEX(0,1))": "-2+5i", "=IMPRODUCT(A1:C1)": "4", + // MINVERSE + "=MINVERSE(A1:B2)": "", + // MMULT + "=MMULT(A4:A4,A4:A4)": "", // MOD "=MOD(6,4)": "2", "=MOD(6,3)": "0", @@ -2336,7 +2340,17 @@ func TestCalcCellValue(t *testing.T) { "=LOG10()": "LOG10 requires 1 numeric argument", `=LOG10("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // MDETERM - "MDETERM()": "MDETERM requires at least 1 argument", + "=MDETERM()": "MDETERM requires 1 argument", + // MINVERSE + "=MINVERSE()": "MINVERSE requires 1 argument", + "=MINVERSE(B3:C4)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=MINVERSE(A1:C2)": "#VALUE!", + "=MINVERSE(A4:A4)": "#NUM!", + // MMULT + "=MMULT()": "MMULT requires 2 argument", + "=MMULT(A1:B2,B3:C4)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=MMULT(B3:C4,A1:B2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=MMULT(A1:A2,B1:B2)": "#VALUE!", // MOD "=MOD()": "MOD requires 2 numeric arguments", "=MOD(6,0)": "MOD divide by zero", From 66776730b605dfef2d01dd8a59afc45d98272eb1 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 15 Apr 2022 00:27:47 +0800 Subject: [PATCH 029/213] ref #65, new formula functions: PEARSON and RSQ --- calc.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 12 ++++++++++ 2 files changed, 74 insertions(+) diff --git a/calc.go b/calc.go index 907e90ef03..a90bbc3e6d 100644 --- a/calc.go +++ b/calc.go @@ -584,6 +584,7 @@ type formulaFuncs struct { // ODDFPRICE // OR // PDURATION +// PEARSON // PERCENTILE.EXC // PERCENTILE.INC // PERCENTILE @@ -628,6 +629,7 @@ type formulaFuncs struct { // ROW // ROWS // RRI +// RSQ // SEC // SECH // SECOND @@ -8858,6 +8860,56 @@ func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { return newNumberFormulaArg(min) } +// pearsonProduct is an implementation of the formula functions PEARSON and +// RSQ. +func (fn *formulaFuncs) pearsonProduct(name string, argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name)) + } + array1 := argsList.Front().Value.(formulaArg).ToList() + array2 := argsList.Back().Value.(formulaArg).ToList() + if len(array1) != len(array2) { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + var sum, deltaX, deltaY, x, y, length float64 + for i := 0; i < len(array1); i++ { + num1, num2 := array1[i].ToNumber(), array2[i].ToNumber() + if !(num1.Type == ArgNumber && num2.Type == ArgNumber) { + continue + } + x += num1.Number + y += num2.Number + length++ + } + x /= length + y /= length + for i := 0; i < len(array1); i++ { + num1, num2 := array1[i].ToNumber(), array2[i].ToNumber() + if !(num1.Type == ArgNumber && num2.Type == ArgNumber) { + continue + } + sum += (num1.Number - x) * (num2.Number - y) + deltaX += (num1.Number - x) * (num1.Number - x) + deltaY += (num2.Number - y) * (num2.Number - y) + } + if deltaX == 0 || deltaY == 0 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + if name == "RSQ" { + return newNumberFormulaArg(math.Pow(sum/math.Sqrt(deltaX*deltaY), 2)) + } + return newNumberFormulaArg(sum / math.Sqrt(deltaX*deltaY)) +} + +// PEARSON function calculates the Pearson Product-Moment Correlation +// Coefficient for two sets of values. The syntax of the function is: +// +// PEARSON(array1,array2) +// +func (fn *formulaFuncs) PEARSON(argsList *list.List) formulaArg { + return fn.pearsonProduct("PEARSON", argsList) +} + // PERCENTILEdotEXC function returns the k'th percentile (i.e. the value below // which k% of the data values fall) for a supplied range of values and a // supplied k (between 0 & 1 exclusive).The syntax of the function is: @@ -9206,6 +9258,16 @@ func (fn *formulaFuncs) RANK(argsList *list.List) formulaArg { return fn.rank("RANK", argsList) } +// RSQ function calculates the square of the Pearson Product-Moment Correlation +// Coefficient for two supplied sets of values. The syntax of the function +// is: +// +// RSQ(known_y's,known_x's) +// +func (fn *formulaFuncs) RSQ(argsList *list.List) formulaArg { + return fn.pearsonProduct("RSQ", argsList) +} + // SKEW function calculates the skewness of the distribution of a supplied set // of values. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 52bc06187d..a7f88b0e33 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1095,6 +1095,8 @@ func TestCalcCellValue(t *testing.T) { "=MINA(A1:B4,MUNIT(1),INT(0),1,E1:F2,\"\")": "0", // MINIFS "=MINIFS(F2:F4,A2:A4,\">0\")": "22100", + // PEARSON + "=PEARSON(A1:A4,B1:B4)": "1", // PERCENTILE.EXC "=PERCENTILE.EXC(A1:A4,0.2)": "0", "=PERCENTILE.EXC(A1:A4,0.6)": "2", @@ -1149,6 +1151,8 @@ func TestCalcCellValue(t *testing.T) { "=RANK.EQ(1,A1:B5)": "5", "=RANK.EQ(1,A1:B5,0)": "5", "=RANK.EQ(1,A1:B5,1)": "2", + // RSQ + "=RSQ(A1:A4,B1:B4)": "1", // SKEW "=SKEW(1,2,3,4,3)": "-0.404796008910937", "=SKEW(A1:B2)": "0", @@ -2974,6 +2978,10 @@ func TestCalcCellValue(t *testing.T) { // MINIFS "=MINIFS()": "MINIFS requires at least 3 arguments", "=MINIFS(F2:F4,A2:A4,\"<0\",D2:D9)": "#N/A", + // PEARSON + "=PEARSON()": "PEARSON requires 2 arguments", + "=PEARSON(A1:A2,B1:B1)": "#N/A", + "=PEARSON(A4,A4)": "#DIV/0!", // PERCENTILE.EXC "=PERCENTILE.EXC()": "PERCENTILE.EXC requires 2 arguments", "=PERCENTILE.EXC(A1:A4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -3047,6 +3055,10 @@ func TestCalcCellValue(t *testing.T) { "=RANK.EQ(-1,A1:B5)": "#N/A", "=RANK.EQ(\"\",A1:B5)": "strconv.ParseFloat: parsing \"\": invalid syntax", "=RANK.EQ(1,A1:B5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + // RSQ + "=RSQ()": "RSQ requires 2 arguments", + "=RSQ(A1:A2,B1:B1)": "#N/A", + "=RSQ(A4,A4)": "#DIV/0!", // SKEW "=SKEW()": "SKEW requires at least 1 argument", "=SKEW(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", From 5a279321bb494141fb12ac010a33da4a78c6a309 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 15 Apr 2022 03:13:41 -0400 Subject: [PATCH 030/213] added macro functionality to shape (#1182) --- shape.go | 4 +++- xmlDrawing.go | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/shape.go b/shape.go index 514171a8b7..8aefeea846 100644 --- a/shape.go +++ b/shape.go @@ -32,7 +32,8 @@ func parseFormatShapeSet(formatSet string) (*formatShape, error) { XScale: 1.0, YScale: 1.0, }, - Line: formatLine{Width: 1}, + Line: formatLine{Width: 1}, + Macro: "", } err := json.Unmarshal([]byte(formatSet), &format) return &format, err @@ -369,6 +370,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format twoCellAnchor.From = &from twoCellAnchor.To = &to shape := xdrSp{ + Macro: formatSet.Macro, NvSpPr: &xdrNvSpPr{ CNvPr: &xlsxCNvPr{ ID: cNvPrID, diff --git a/xmlDrawing.go b/xmlDrawing.go index 4bf43ec89b..d6d6135180 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -477,6 +477,7 @@ type formatPicture struct { // formatShape directly maps the format settings of the shape. type formatShape struct { + Macro string `json:"macro"` Type string `json:"type"` Width int `json:"width"` Height int `json:"height"` From 6fa950a4f852bd45b81c941877732ec516dcc673 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 16 Apr 2022 13:53:16 +0800 Subject: [PATCH 031/213] ref #65, new formula functions: SKEW.P and SLOPE, remove no-required format default --- calc.go | 74 +++++++++++++++++++++++++++++++-------- calc_test.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++ chart.go | 7 +--- picture.go | 5 --- shape.go | 7 +--- table.go | 5 +-- 6 files changed, 160 insertions(+), 35 deletions(-) diff --git a/calc.go b/calc.go index a90bbc3e6d..03d467b851 100644 --- a/calc.go +++ b/calc.go @@ -640,7 +640,9 @@ type formulaFuncs struct { // SIN // SINH // SKEW +// SKEW.P // SLN +// SLOPE // SMALL // SQRT // SQRTPI @@ -8860,14 +8862,20 @@ func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { return newNumberFormulaArg(min) } -// pearsonProduct is an implementation of the formula functions PEARSON and -// RSQ. +// pearsonProduct is an implementation of the formula functions PEARSON, RSQ +// and SLOPE. func (fn *formulaFuncs) pearsonProduct(name string, argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name)) } - array1 := argsList.Front().Value.(formulaArg).ToList() - array2 := argsList.Back().Value.(formulaArg).ToList() + var array1, array2 []formulaArg + if name == "SLOPE" { + array1 = argsList.Back().Value.(formulaArg).ToList() + array2 = argsList.Front().Value.(formulaArg).ToList() + } else { + array1 = argsList.Front().Value.(formulaArg).ToList() + array2 = argsList.Back().Value.(formulaArg).ToList() + } if len(array1) != len(array2) { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } @@ -8898,7 +8906,10 @@ func (fn *formulaFuncs) pearsonProduct(name string, argsList *list.List) formula if name == "RSQ" { return newNumberFormulaArg(math.Pow(sum/math.Sqrt(deltaX*deltaY), 2)) } - return newNumberFormulaArg(sum / math.Sqrt(deltaX*deltaY)) + if name == "PEARSON" { + return newNumberFormulaArg(sum / math.Sqrt(deltaX*deltaY)) + } + return newNumberFormulaArg(sum / deltaX) } // PEARSON function calculates the Pearson Product-Moment Correlation @@ -9268,16 +9279,19 @@ func (fn *formulaFuncs) RSQ(argsList *list.List) formulaArg { return fn.pearsonProduct("RSQ", argsList) } -// SKEW function calculates the skewness of the distribution of a supplied set -// of values. The syntax of the function is: -// -// SKEW(number1,[number2],...) -// -func (fn *formulaFuncs) SKEW(argsList *list.List) formulaArg { +// skew is an implementation of the formula functions SKEW and SKEW.P. +func (fn *formulaFuncs) skew(name string, argsList *list.List) formulaArg { if argsList.Len() < 1 { - return newErrorFormulaArg(formulaErrorVALUE, "SKEW requires at least 1 argument") + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name)) + } + mean := fn.AVERAGE(argsList) + var stdDev formulaArg + var count, summer float64 + if name == "SKEW" { + stdDev = fn.STDEV(argsList) + } else { + stdDev = fn.STDEVP(argsList) } - mean, stdDev, count, summer := fn.AVERAGE(argsList), fn.STDEV(argsList), 0.0, 0.0 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { @@ -9300,11 +9314,43 @@ func (fn *formulaFuncs) SKEW(argsList *list.List) formulaArg { } } if count > 2 { - return newNumberFormulaArg(summer * (count / ((count - 1) * (count - 2)))) + if name == "SKEW" { + return newNumberFormulaArg(summer * (count / ((count - 1) * (count - 2)))) + } + return newNumberFormulaArg(summer / count) } return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } +// SKEW function calculates the skewness of the distribution of a supplied set +// of values. The syntax of the function is: +// +// SKEW(number1,[number2],...) +// +func (fn *formulaFuncs) SKEW(argsList *list.List) formulaArg { + return fn.skew("SKEW", argsList) +} + +// SKEWdotP function calculates the skewness of the distribution of a supplied +// set of values. The syntax of the function is: +// +// SKEW.P(number1,[number2],...) +// +func (fn *formulaFuncs) SKEWdotP(argsList *list.List) formulaArg { + return fn.skew("SKEW.P", argsList) +} + +// SLOPE returns the slope of the linear regression line through data points in +// known_y's and known_x's. The slope is the vertical distance divided by the +// horizontal distance between any two points on the line, which is the rate +// of change along the regression line. The syntax of the function is: +// +// SLOPE(known_y's,known_x's) +// +func (fn *formulaFuncs) SLOPE(argsList *list.List) formulaArg { + return fn.pearsonProduct("SLOPE", argsList) +} + // SMALL function returns the k'th smallest value from an array of numeric // values. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index a7f88b0e33..6cae4a3ff7 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1157,6 +1157,12 @@ func TestCalcCellValue(t *testing.T) { "=SKEW(1,2,3,4,3)": "-0.404796008910937", "=SKEW(A1:B2)": "0", "=SKEW(A1:D3)": "0", + // SKEW.P + "=SKEW.P(1,2,3,4,3)": "-0.27154541788364", + "=SKEW.P(A1:B2)": "0", + "=SKEW.P(A1:D3)": "0", + // SLOPE + "=SLOPE(A1:A4,B1:B4)": "1", // SMALL "=SMALL(A1:A5,1)": "0", "=SMALL(A1:B5,2)": "1", @@ -3063,6 +3069,14 @@ func TestCalcCellValue(t *testing.T) { "=SKEW()": "SKEW requires at least 1 argument", "=SKEW(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=SKEW(0)": "#DIV/0!", + // SKEW.P + "=SKEW.P()": "SKEW.P requires at least 1 argument", + "=SKEW.P(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=SKEW.P(0)": "#DIV/0!", + // SLOPE + "=SLOPE()": "SLOPE requires 2 arguments", + "=SLOPE(A1:A2,B1:B1)": "#N/A", + "=SLOPE(A4,A4)": "#DIV/0!", // SMALL "=SMALL()": "SMALL requires 2 arguments", "=SMALL(A1:A5,0)": "k should be > 0", @@ -4968,6 +4982,89 @@ func TestCalcMODE(t *testing.T) { } } +func TestCalcPEARSON(t *testing.T) { + cellData := [][]interface{}{ + {"x", "y"}, + {1, 10.11}, + {2, 22.9}, + {2, 27.61}, + {3, 27.61}, + {4, 11.15}, + {5, 31.08}, + {6, 37.9}, + {7, 33.49}, + {8, 21.05}, + {9, 27.01}, + {10, 45.78}, + {11, 31.32}, + {12, 50.57}, + {13, 45.48}, + {14, 40.94}, + {15, 53.76}, + {16, 36.18}, + {17, 49.77}, + {18, 55.66}, + {19, 63.83}, + {20, 63.6}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=PEARSON(A2:A22,B2:B22)": "0.864129542184994", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } +} + +func TestCalcRSQ(t *testing.T) { + cellData := [][]interface{}{ + {"known_y's", "known_x's"}, + {2, 22.9}, + {7, 33.49}, + {8, 34.5}, + {3, 27.61}, + {4, 19.5}, + {1, 10.11}, + {6, 37.9}, + {5, 31.08}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=RSQ(A2:A9,B2:B9)": "0.711666290486784", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } +} + +func TestCalcSLOP(t *testing.T) { + cellData := [][]interface{}{ + {"known_x's", "known_y's"}, + {1, 3}, + {2, 7}, + {3, 17}, + {4, 20}, + {5, 20}, + {6, 27}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=SLOPE(A2:A7,B2:B7)": "0.200826446280992", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } +} + func TestCalcSHEET(t *testing.T) { f := NewFile() f.NewSheet("Sheet2") diff --git a/chart.go b/chart.go index 8f521fa048..f740a2b219 100644 --- a/chart.go +++ b/chart.go @@ -479,16 +479,11 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { }, Format: formatPicture{ FPrintsWithSheet: true, - FLocksWithSheet: false, - NoChangeAspect: false, - OffsetX: 0, - OffsetY: 0, XScale: 1.0, YScale: 1.0, }, Legend: formatChartLegend{ - Position: "bottom", - ShowLegendKey: false, + Position: "bottom", }, Title: formatChartTitle{ Name: " ", diff --git a/picture.go b/picture.go index 515f15f942..919262c99d 100644 --- a/picture.go +++ b/picture.go @@ -31,11 +31,6 @@ import ( func parseFormatPictureSet(formatSet string) (*formatPicture, error) { format := formatPicture{ FPrintsWithSheet: true, - FLocksWithSheet: false, - NoChangeAspect: false, - Autofit: false, - OffsetX: 0, - OffsetY: 0, XScale: 1.0, YScale: 1.0, } diff --git a/shape.go b/shape.go index 8aefeea846..db76867355 100644 --- a/shape.go +++ b/shape.go @@ -25,15 +25,10 @@ func parseFormatShapeSet(formatSet string) (*formatShape, error) { Height: 160, Format: formatPicture{ FPrintsWithSheet: true, - FLocksWithSheet: false, - NoChangeAspect: false, - OffsetX: 0, - OffsetY: 0, XScale: 1.0, YScale: 1.0, }, - Line: formatLine{Width: 1}, - Macro: "", + Line: formatLine{Width: 1}, } err := json.Unmarshal([]byte(formatSet), &format) return &format, err diff --git a/table.go b/table.go index 0311a8ed11..b01c1cb7e8 100644 --- a/table.go +++ b/table.go @@ -23,10 +23,7 @@ import ( // parseFormatTableSet provides a function to parse the format settings of the // table with default value. func parseFormatTableSet(formatSet string) (*formatTable, error) { - format := formatTable{ - TableStyle: "", - ShowRowStripes: true, - } + format := formatTable{ShowRowStripes: true} err := json.Unmarshal(parseFormatSet(formatSet), &format) return &format, err } From 0b8965dba9cf98fd1f5704ed0d354504c20776fa Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 17 Apr 2022 23:49:37 +0800 Subject: [PATCH 032/213] ref #65, initial formula functions: GROWTH and TREND --- calc.go | 825 +++++++++++++++++++++++++++++++++++++++++++++++---- calc_test.go | 85 +++++- 2 files changed, 848 insertions(+), 62 deletions(-) diff --git a/calc.go b/calc.go index 03d467b851..f4ab9c4a72 100644 --- a/calc.go +++ b/calc.go @@ -462,6 +462,7 @@ type formulaFuncs struct { // GCD // GEOMEAN // GESTEP +// GROWTH // HARMEAN // HEX2BIN // HEX2DEC @@ -682,6 +683,7 @@ type formulaFuncs struct { // TINV // TODAY // TRANSPOSE +// TREND // TRIM // TRIMMEAN // TRUE @@ -960,7 +962,11 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, argsStack.Peek().(*list.List).PushBack(arg) } } else { - opdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + val := arg.Value() + if arg.Type == ArgMatrix && len(arg.Matrix) > 0 && len(arg.Matrix[0]) > 0 { + val = arg.Matrix[0][0].Value() + } + opdStack.Push(efp.Token{TValue: val, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } return nil } @@ -2015,7 +2021,6 @@ func (fn *formulaFuncs) COMPLEX(argsList *list.List) formulaArg { // cmplx2str replace complex number string characters. func cmplx2str(num complex128, suffix string) string { - c := fmt.Sprint(num) realPart, imagPart := fmt.Sprint(real(num)), fmt.Sprint(imag(num)) isNum, i := isNumeric(realPart) if isNum && i > 15 { @@ -2025,7 +2030,7 @@ func cmplx2str(num complex128, suffix string) string { if isNum && i > 15 { imagPart = roundPrecision(imagPart, -1) } - c = realPart + c := realPart if imag(num) > 0 { c += "+" } @@ -4073,7 +4078,8 @@ func newFormulaArgMatrix(numMtx [][]float64) (arg [][]formulaArg) { for r, row := range numMtx { arg = append(arg, make([]formulaArg, len(row))) for c, cell := range row { - arg[r][c] = newNumberFormulaArg(cell) + decimal, _ := big.NewFloat(cell).SetPrec(15).Float64() + arg[r][c] = newNumberFormulaArg(decimal) } } return @@ -5819,12 +5825,12 @@ func (fn *formulaFuncs) BETAdotDIST(argsList *list.List) formulaArg { if a.Number == b.Number { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - fScale := b.Number - a.Number - x.Number = (x.Number - a.Number) / fScale + scale := b.Number - a.Number + x.Number = (x.Number - a.Number) / scale if cumulative.Number == 1 { return newNumberFormulaArg(getBetaDist(x.Number, alpha.Number, beta.Number)) } - return newNumberFormulaArg(getBetaDistPDF(x.Number, alpha.Number, beta.Number) / fScale) + return newNumberFormulaArg(getBetaDistPDF(x.Number, alpha.Number, beta.Number) / scale) } // BETADIST function calculates the cumulative beta probability density @@ -6665,12 +6671,12 @@ func getLogGamma(fZ float64) float64 { // getLowRegIGamma returns lower regularized incomplete gamma function. func getLowRegIGamma(fA, fX float64) float64 { - fLnFactor := fA*math.Log(fX) - fX - getLogGamma(fA) - fFactor := math.Exp(fLnFactor) + lnFactor := fA*math.Log(fX) - fX - getLogGamma(fA) + factor := math.Exp(lnFactor) if fX > fA+1 { - return 1 - fFactor*getGammaContFraction(fA, fX) + return 1 - factor*getGammaContFraction(fA, fX) } - return fFactor * getGammaSeries(fA, fX) + return factor * getGammaSeries(fA, fX) } // getChiSqDistCDF returns left tail for the Chi-Square distribution. @@ -7610,6 +7616,703 @@ func (fn *formulaFuncs) GEOMEAN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } +// getNewMatrix create matrix by given columns and rows. +func getNewMatrix(c, r int) (matrix [][]float64) { + for i := 0; i < c; i++ { + for j := 0; j < r; j++ { + for x := len(matrix); x <= i; x++ { + matrix = append(matrix, []float64{}) + } + for y := len(matrix[i]); y <= j; y++ { + matrix[i] = append(matrix[i], 0) + } + matrix[i][j] = 0 + } + } + return +} + +// approxSub subtract two values, if signs are identical and the values are +// equal, will be returns 0 instead of calculating the subtraction. +func approxSub(a, b float64) float64 { + if ((a < 0 && b < 0) || (a > 0 && b > 0)) && math.Abs(a-b) < 2.22045e-016 { + return 0 + } + return a - b +} + +// matrixClone return a copy of all elements of the original matrix. +func matrixClone(matrix [][]float64) (cloneMatrix [][]float64) { + for i := 0; i < len(matrix); i++ { + for j := 0; j < len(matrix[i]); j++ { + for x := len(cloneMatrix); x <= i; x++ { + cloneMatrix = append(cloneMatrix, []float64{}) + } + for k := len(cloneMatrix[i]); k <= j; k++ { + cloneMatrix[i] = append(cloneMatrix[i], 0) + } + cloneMatrix[i][j] = matrix[i][j] + } + } + return +} + +// trendGrowthMatrixInfo defined matrix checking result. +type trendGrowthMatrixInfo struct { + trendType, nCX, nCY, nRX, nRY, M, N int + mtxX, mtxY [][]float64 +} + +// prepareTrendGrowthMtxX is a part of implementation of the trend growth prepare. +func prepareTrendGrowthMtxX(mtxX [][]float64) [][]float64 { + var mtx [][]float64 + for i := 0; i < len(mtxX); i++ { + for j := 0; j < len(mtxX[i]); j++ { + if mtxX[i][j] == 0 { + return nil + } + for x := len(mtx); x <= j; x++ { + mtx = append(mtx, []float64{}) + } + for y := len(mtx[j]); y <= i; y++ { + mtx[j] = append(mtx[j], 0) + } + mtx[j][i] = mtxX[i][j] + } + } + return mtx +} + +// prepareTrendGrowthMtxY is a part of implementation of the trend growth prepare. +func prepareTrendGrowthMtxY(bLOG bool, mtxY [][]float64) [][]float64 { + var mtx [][]float64 + for i := 0; i < len(mtxY); i++ { + for j := 0; j < len(mtxY[i]); j++ { + if mtxY[i][j] == 0 { + return nil + } + for x := len(mtx); x <= j; x++ { + mtx = append(mtx, []float64{}) + } + for y := len(mtx[j]); y <= i; y++ { + mtx[j] = append(mtx[j], 0) + } + mtx[j][i] = mtxY[i][j] + } + } + if bLOG { + var pNewY [][]float64 + for i := 0; i < len(mtxY); i++ { + for j := 0; j < len(mtxY[i]); j++ { + fVal := mtxY[i][j] + if fVal <= 0 { + return nil + } + for x := len(pNewY); x <= j; x++ { + pNewY = append(pNewY, []float64{}) + } + for y := len(pNewY[j]); y <= i; y++ { + pNewY[j] = append(pNewY[j], 0) + } + pNewY[j][i] = math.Log(fVal) + } + } + mtx = pNewY + } + return mtx +} + +// prepareTrendGrowth check and return the result. +func prepareTrendGrowth(bLOG bool, mtxX, mtxY [][]float64) (*trendGrowthMatrixInfo, formulaArg) { + var nCX, nRX, M, N, trendType int + nRY, nCY := len(mtxY), len(mtxY[0]) + cntY := nCY * nRY + newY := prepareTrendGrowthMtxY(bLOG, mtxY) + if newY == nil { + return nil, newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + var newX [][]float64 + if len(mtxX) != 0 { + nRX, nCX = len(mtxX), len(mtxX[0]) + if newX = prepareTrendGrowthMtxX(mtxX); newX == nil { + return nil, newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if nCX == nCY && nRX == nRY { + trendType, M, N = 1, 1, cntY // simple regression + } else if nCY != 1 && nRY != 1 { + return nil, newErrorFormulaArg(formulaErrorREF, formulaErrorREF) + } else if nCY == 1 { + if nRX != nRY { + return nil, newErrorFormulaArg(formulaErrorREF, formulaErrorREF) + } + trendType, M, N = 2, nCX, nRY + } else if nCX != nCY { + return nil, newErrorFormulaArg(formulaErrorREF, formulaErrorREF) + } else { + trendType, M, N = 3, nRX, nCY + } + } else { + newX = getNewMatrix(nCY, nRY) + nCX, nRX = nCY, nRY + num := 1.0 + for i := 0; i < nRY; i++ { + for j := 0; j < nCY; j++ { + newX[j][i] = num + num++ + } + } + trendType, M, N = 1, 1, cntY + } + return &trendGrowthMatrixInfo{ + trendType: trendType, + nCX: nCX, + nCY: nCY, + nRX: nRX, + nRY: nRY, + M: M, + N: N, + mtxX: newX, + mtxY: newY, + }, newEmptyFormulaArg() +} + +// calcPosition calculate position for matrix by given index. +func calcPosition(mtx [][]float64, idx int) (row, col int) { + rowSize := len(mtx[0]) + col = idx + if rowSize > 1 { + col = idx / rowSize + } + row = idx - col*rowSize + return +} + +// getDouble returns float64 data type value in the matrix by given index. +func getDouble(mtx [][]float64, idx int) float64 { + row, col := calcPosition(mtx, idx) + return mtx[col][row] +} + +// putDouble set a float64 data type value in the matrix by given index. +func putDouble(mtx [][]float64, idx int, val float64) { + row, col := calcPosition(mtx, idx) + mtx[col][row] = val +} + +// calcMeanOverAll returns mean of the given matrix by over all element. +func calcMeanOverAll(mtx [][]float64, n int) float64 { + var sum float64 + for i := 0; i < len(mtx); i++ { + for j := 0; j < len(mtx[i]); j++ { + sum += mtx[i][j] + } + } + return sum / float64(n) +} + +// calcSumProduct returns uses the matrices as vectors of length M over all +// element. +func calcSumProduct(mtxA, mtxB [][]float64, m int) float64 { + sum := 0.0 + for i := 0; i < m; i++ { + sum += getDouble(mtxA, i) * getDouble(mtxB, i) + } + return sum +} + +// calcColumnMeans calculates means of the columns of matrix. +func calcColumnMeans(mtxX, mtxRes [][]float64, c, r int) { + for i := 0; i < c; i++ { + var sum float64 + for k := 0; k < r; k++ { + sum += mtxX[i][k] + } + putDouble(mtxRes, i, sum/float64(r)) + } + return +} + +// calcColumnsDelta calculates subtract of the columns of matrix. +func calcColumnsDelta(mtx, columnMeans [][]float64, c, r int) { + for i := 0; i < c; i++ { + for k := 0; k < r; k++ { + mtx[i][k] = approxSub(mtx[i][k], getDouble(columnMeans, i)) + } + } +} + +// calcSign returns sign by given value, no mathematical signum, but used to +// switch between adding and subtracting. +func calcSign(val float64) float64 { + if val > 0 { + return 1 + } + return -1 +} + +// calcColsMaximumNorm is a special version for use within QR +// decomposition. Maximum norm of column index c starting in row index r; +// matrix A has count n rows. +func calcColsMaximumNorm(mtxA [][]float64, c, r, n int) float64 { + var norm float64 + for row := r; row < n; row++ { + if norm < math.Abs(mtxA[c][row]) { + norm = math.Abs(mtxA[c][row]) + } + } + return norm +} + +// calcFastMult returns multiply n x m matrix A with m x l matrix B to n x l matrix R. +func calcFastMult(mtxA, mtxB, mtxR [][]float64, n, m, l int) { + var sum float64 + for row := 0; row < n; row++ { + for col := 0; col < l; col++ { + sum = 0.0 + for k := 0; k < m; k++ { + sum += mtxA[k][row] * mtxB[col][k] + } + mtxR[col][row] = sum + } + } +} + +// calcRowsEuclideanNorm is a special version for use within QR +// decomposition. Euclidean norm of column index c starting in row index r; +// matrix a has count n rows. +func calcRowsEuclideanNorm(mtxA [][]float64, c, r, n int) float64 { + var norm float64 + for row := r; row < n; row++ { + norm += mtxA[c][row] * mtxA[c][row] + } + return math.Sqrt(norm) +} + +// calcRowsSumProduct is a special version for use within QR decomposition. +// starting in row index r; +// a and b are indices of columns, matrices A and B have count n rows. +func calcRowsSumProduct(mtxA [][]float64, a int, mtxB [][]float64, b, r, n int) float64 { + var result float64 + for row := r; row < n; row++ { + result += mtxA[a][row] * mtxB[b][row] + } + return result +} + +// calcSolveWithUpperRightTriangle solve for X in R*X=S using back substitution. +func calcSolveWithUpperRightTriangle(mtxA [][]float64, vecR []float64, mtxS [][]float64, k int, bIsTransposed bool) { + var row int + for rowp1 := k; rowp1 > 0; rowp1-- { + row = rowp1 - 1 + sum := getDouble(mtxS, row) + for col := rowp1; col < k; col++ { + if bIsTransposed { + sum -= mtxA[row][col] * getDouble(mtxS, col) + } else { + sum -= mtxA[col][row] * getDouble(mtxS, col) + } + } + putDouble(mtxS, row, sum/vecR[row]) + } +} + +// calcRowQRDecomposition calculates a QR decomposition with Householder +// reflection. +func calcRowQRDecomposition(mtxA [][]float64, vecR []float64, k, n int) bool { + for col := 0; col < k; col++ { + scale := calcColsMaximumNorm(mtxA, col, col, n) + if scale == 0 { + return false + } + for row := col; row < n; row++ { + mtxA[col][row] = mtxA[col][row] / scale + } + euclid := calcRowsEuclideanNorm(mtxA, col, col, n) + factor := 1.0 / euclid / (euclid + math.Abs(mtxA[col][col])) + signum := calcSign(mtxA[col][col]) + mtxA[col][col] = mtxA[col][col] + signum*euclid + vecR[col] = -signum * scale * euclid + // apply Householder transformation to A + for c := col + 1; c < k; c++ { + sum := calcRowsSumProduct(mtxA, col, mtxA, c, col, n) + for row := col; row < n; row++ { + mtxA[c][row] = mtxA[c][row] - sum*factor*mtxA[col][row] + } + } + } + return true +} + +// calcApplyColsHouseholderTransformation transposed matrices A and Y. +func calcApplyColsHouseholderTransformation(mtxA [][]float64, r int, mtxY [][]float64, n int) { + denominator := calcColsSumProduct(mtxA, r, mtxA, r, r, n) + numerator := calcColsSumProduct(mtxA, r, mtxY, 0, r, n) + factor := 2 * (numerator / denominator) + for col := r; col < n; col++ { + putDouble(mtxY, col, getDouble(mtxY, col)-factor*mtxA[col][r]) + } +} + +// calcRowMeans calculates means of the rows of matrix. +func calcRowMeans(mtxX, mtxRes [][]float64, c, r int) { + for k := 0; k < r; k++ { + var fSum float64 + for i := 0; i < c; i++ { + fSum += mtxX[i][k] + } + mtxRes[k][0] = fSum / float64(c) + } +} + +// calcRowsDelta calculates subtract of the rows of matrix. +func calcRowsDelta(mtx, rowMeans [][]float64, c, r int) { + for k := 0; k < r; k++ { + for i := 0; i < c; i++ { + mtx[i][k] = approxSub(mtx[i][k], rowMeans[k][0]) + } + } +} + +// calcColumnMaximumNorm returns maximum norm of row index R starting in col +// index C; matrix A has count N columns. +func calcColumnMaximumNorm(mtxA [][]float64, r, c, n int) float64 { + var norm float64 + for col := c; col < n; col++ { + if norm < math.Abs(mtxA[col][r]) { + norm = math.Abs(mtxA[col][r]) + } + } + return norm +} + +// calcColsEuclideanNorm returns euclidean norm of row index R starting in +// column index C; matrix A has count N columns. +func calcColsEuclideanNorm(mtxA [][]float64, r, c, n int) float64 { + var norm float64 + for col := c; col < n; col++ { + norm += (mtxA[col][r]) * (mtxA[col][r]) + } + return math.Sqrt(norm) +} + +// calcColsSumProduct returns sum product for given matrix. +func calcColsSumProduct(mtxA [][]float64, a int, mtxB [][]float64, b, c, n int) float64 { + var result float64 + for col := c; col < n; col++ { + result += mtxA[col][a] * mtxB[col][b] + } + return result +} + +// calcColQRDecomposition same with transposed matrix A, N is count of +// columns, k count of rows. +func calcColQRDecomposition(mtxA [][]float64, vecR []float64, k, n int) bool { + var sum float64 + for row := 0; row < k; row++ { + // calculate vector u of the householder transformation + scale := calcColumnMaximumNorm(mtxA, row, row, n) + if scale == 0 { + return false + } + for col := row; col < n; col++ { + mtxA[col][row] = mtxA[col][row] / scale + } + euclid := calcColsEuclideanNorm(mtxA, row, row, n) + factor := 1 / euclid / (euclid + math.Abs(mtxA[row][row])) + signum := calcSign(mtxA[row][row]) + mtxA[row][row] = mtxA[row][row] + signum*euclid + vecR[row] = -signum * scale * euclid + // apply Householder transformation to A + for r := row + 1; r < k; r++ { + sum = calcColsSumProduct(mtxA, row, mtxA, r, row, n) + for col := row; col < n; col++ { + mtxA[col][r] = mtxA[col][r] - sum*factor*mtxA[col][row] + } + } + } + return true +} + +// calcApplyRowsHouseholderTransformation applies a Householder transformation to a +// column vector Y with is given as Nx1 Matrix. The vector u, from which the +// Householder transformation is built, is the column part in matrix A, with +// column index c, starting with row index c. A is the result of the QR +// decomposition as obtained from calcRowQRDecomposition. +func calcApplyRowsHouseholderTransformation(mtxA [][]float64, c int, mtxY [][]float64, n int) { + denominator := calcRowsSumProduct(mtxA, c, mtxA, c, c, n) + numerator := calcRowsSumProduct(mtxA, c, mtxY, 0, c, n) + factor := 2 * (numerator / denominator) + for row := c; row < n; row++ { + putDouble(mtxY, row, getDouble(mtxY, row)-factor*mtxA[c][row]) + } +} + +// calcTrendGrowthSimpleRegression calculate simple regression for the calcTrendGrowth. +func calcTrendGrowthSimpleRegression(bConstant, bGrowth bool, mtxY, mtxX, newX, mtxRes [][]float64, meanY float64, N int) { + var meanX float64 + if bConstant { + meanX = calcMeanOverAll(mtxX, N) + for i := 0; i < len(mtxX); i++ { + for j := 0; j < len(mtxX[i]); j++ { + mtxX[i][j] = approxSub(mtxX[i][j], meanX) + } + } + } + sumXY := calcSumProduct(mtxX, mtxY, N) + sumX2 := calcSumProduct(mtxX, mtxX, N) + slope := sumXY / sumX2 + var help float64 + var intercept float64 + if bConstant { + intercept = meanY - slope*meanX + for i := 0; i < len(mtxRes); i++ { + for j := 0; j < len(mtxRes[i]); j++ { + help = newX[i][j]*slope + intercept + if bGrowth { + mtxRes[i][j] = math.Exp(help) + } else { + mtxRes[i][j] = help + } + } + } + } else { + for i := 0; i < len(mtxRes); i++ { + for j := 0; j < len(mtxRes[i]); j++ { + help = newX[i][j] * slope + if bGrowth { + mtxRes[i][j] = math.Exp(help) + } else { + mtxRes[i][j] = help + } + } + } + } +} + +// calcTrendGrowthMultipleRegressionPart1 calculate multiple regression for the +// calcTrendGrowth. +func calcTrendGrowthMultipleRegressionPart1(bConstant, bGrowth bool, mtxY, mtxX, newX, mtxRes [][]float64, meanY float64, RXN, K, N int) { + vecR := make([]float64, N) // for QR decomposition + means := getNewMatrix(K, 1) // mean of each column + slopes := getNewMatrix(1, K) // from b1 to bK + if len(means) == 0 || len(slopes) == 0 { + return + } + if bConstant { + calcColumnMeans(mtxX, means, K, N) + calcColumnsDelta(mtxX, means, K, N) + } + if !calcRowQRDecomposition(mtxX, vecR, K, N) { + return + } + // Later on we will divide by elements of vecR, so make sure that they aren't zero. + bIsSingular := false + for row := 0; row < K && !bIsSingular; row++ { + bIsSingular = bIsSingular || vecR[row] == 0 + } + if bIsSingular { + return + } + for col := 0; col < K; col++ { + calcApplyRowsHouseholderTransformation(mtxX, col, mtxY, N) + } + for col := 0; col < K; col++ { + putDouble(slopes, col, getDouble(mtxY, col)) + } + calcSolveWithUpperRightTriangle(mtxX, vecR, slopes, K, false) + // Fill result matrix + calcFastMult(newX, slopes, mtxRes, RXN, K, 1) + if bConstant { + intercept := meanY - calcSumProduct(means, slopes, K) + for row := 0; row < RXN; row++ { + mtxRes[0][row] = mtxRes[0][row] + intercept + } + } + if bGrowth { + for i := 0; i < RXN; i++ { + putDouble(mtxRes, i, math.Exp(getDouble(mtxRes, i))) + } + } +} + +// calcTrendGrowthMultipleRegressionPart2 calculate multiple regression for the +// calcTrendGrowth. +func calcTrendGrowthMultipleRegressionPart2(bConstant, bGrowth bool, mtxY, mtxX, newX, mtxRes [][]float64, meanY float64, nCXN, K, N int) { + vecR := make([]float64, N) // for QR decomposition + means := getNewMatrix(K, 1) // mean of each row + slopes := getNewMatrix(K, 1) // row from b1 to bK + if len(means) == 0 || len(slopes) == 0 { + return + } + if bConstant { + calcRowMeans(mtxX, means, N, K) + calcRowsDelta(mtxX, means, N, K) + } + if !calcColQRDecomposition(mtxX, vecR, K, N) { + return + } + // later on we will divide by elements of vecR, so make sure that they aren't zero + bIsSingular := false + for row := 0; row < K && !bIsSingular; row++ { + bIsSingular = bIsSingular || vecR[row] == 0 + } + if bIsSingular { + return + } + for row := 0; row < K; row++ { + calcApplyColsHouseholderTransformation(mtxX, row, mtxY, N) + } + for col := 0; col < K; col++ { + putDouble(slopes, col, getDouble(mtxY, col)) + } + calcSolveWithUpperRightTriangle(mtxX, vecR, slopes, K, true) + // fill result matrix + calcFastMult(slopes, newX, mtxRes, 1, K, nCXN) + if bConstant { + fIntercept := meanY - calcSumProduct(means, slopes, K) + for col := 0; col < nCXN; col++ { + mtxRes[col][0] = mtxRes[col][0] + fIntercept + } + } + if bGrowth { + for i := 0; i < nCXN; i++ { + putDouble(mtxRes, i, math.Exp(getDouble(mtxRes, i))) + } + } +} + +// calcTrendGrowthRegression is a part of implementation of the calcTrendGrowth. +func calcTrendGrowthRegression(bConstant, bGrowth bool, trendType, nCXN, nRXN, K, N int, mtxY, mtxX, newX, mtxRes [][]float64) { + if len(mtxRes) == 0 { + return + } + var meanY float64 + if bConstant { + copyX, copyY := matrixClone(mtxX), matrixClone(mtxY) + mtxX, mtxY = copyX, copyY + meanY = calcMeanOverAll(mtxY, N) + for i := 0; i < len(mtxY); i++ { + for j := 0; j < len(mtxY[i]); j++ { + mtxY[i][j] = approxSub(mtxY[i][j], meanY) + } + } + } + switch trendType { + case 1: + calcTrendGrowthSimpleRegression(bConstant, bGrowth, mtxY, mtxX, newX, mtxRes, meanY, N) + break + case 2: + calcTrendGrowthMultipleRegressionPart1(bConstant, bGrowth, mtxY, mtxX, newX, mtxRes, meanY, nRXN, K, N) + break + default: + calcTrendGrowthMultipleRegressionPart2(bConstant, bGrowth, mtxY, mtxX, newX, mtxRes, meanY, nCXN, K, N) + } +} + +// calcTrendGrowth returns values along a predicted exponential trend. +func calcTrendGrowth(mtxY, mtxX, newX [][]float64, bConstant, bGrowth bool) ([][]float64, formulaArg) { + getMatrixParams, errArg := prepareTrendGrowth(bGrowth, mtxX, mtxY) + if errArg.Type != ArgEmpty { + return nil, errArg + } + trendType := getMatrixParams.trendType + nCX := getMatrixParams.nCX + nRX := getMatrixParams.nRX + K := getMatrixParams.M + N := getMatrixParams.N + mtxX = getMatrixParams.mtxX + mtxY = getMatrixParams.mtxY + // checking if data samples are enough + if (bConstant && (N < K+1)) || (!bConstant && (N < K)) || (N < 1) || (K < 1) { + return nil, errArg + } + // set the default newX if necessary + nCXN, nRXN := nCX, nRX + if len(newX) == 0 { + newX = matrixClone(mtxX) // mtxX will be changed to X-meanX + } else { + nRXN, nCXN = len(newX[0]), len(newX) + if (trendType == 2 && K != nCXN) || (trendType == 3 && K != nRXN) { + return nil, errArg + } + } + var mtxRes [][]float64 + switch trendType { + case 1: + mtxRes = getNewMatrix(nCXN, nRXN) + break + case 2: + mtxRes = getNewMatrix(1, nRXN) + break + default: + mtxRes = getNewMatrix(nCXN, 1) + } + calcTrendGrowthRegression(bConstant, bGrowth, trendType, nCXN, nRXN, K, N, mtxY, mtxX, newX, mtxRes) + return mtxRes, errArg +} + +// trendGrowth is an implementation of the formula functions GROWTH and TREND. +func (fn *formulaFuncs) trendGrowth(name string, argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name)) + } + if argsList.Len() > 4 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 4 arguments", name)) + } + var knowY, knowX, newX [][]float64 + var errArg formulaArg + constArg := newBoolFormulaArg(true) + knowY, errArg = newNumberMatrix(argsList.Front().Value.(formulaArg), false) + if errArg.Type == ArgError { + return errArg + } + if argsList.Len() > 1 { + knowX, errArg = newNumberMatrix(argsList.Front().Next().Value.(formulaArg), false) + if errArg.Type == ArgError { + return errArg + } + } + if argsList.Len() > 2 { + newX, errArg = newNumberMatrix(argsList.Front().Next().Next().Value.(formulaArg), false) + if errArg.Type == ArgError { + return errArg + } + } + if argsList.Len() > 3 { + if constArg = argsList.Back().Value.(formulaArg).ToBool(); constArg.Type != ArgNumber { + return constArg + } + } + var mtxNewX [][]float64 + for i := 0; i < len(newX); i++ { + for j := 0; j < len(newX[i]); j++ { + for x := len(mtxNewX); x <= j; x++ { + mtxNewX = append(mtxNewX, []float64{}) + } + for k := len(mtxNewX[j]); k <= i; k++ { + mtxNewX[j] = append(mtxNewX[j], 0) + } + mtxNewX[j][i] = newX[i][j] + } + } + mtx, errArg := calcTrendGrowth(knowY, knowX, mtxNewX, constArg.Number == 1, name == "GROWTH") + if errArg.Type != ArgEmpty { + return errArg + } + return newMatrixFormulaArg(newFormulaArgMatrix(mtx)) +} + +// GROWTH function calculates the exponential growth curve through a given set +// of y-values and (optionally), one or more sets of x-values. The function +// then extends the curve to calculate additional y-values for a further +// supplied set of new x-values. The syntax of the function is: +// +// GROWTH(known_y's,[known_x's],[new_x's],[const]) +// +func (fn *formulaFuncs) GROWTH(argsList *list.List) formulaArg { + return fn.trendGrowth("GROWTH", argsList) +} + // HARMEAN function calculates the harmonic mean of a supplied set of values. // The syntax of the function is: // @@ -9652,92 +10355,98 @@ func (fn *formulaFuncs) TINV(argsList *list.List) formulaArg { return fn.TdotINVdot2T(argsList) } +// TREND function calculates the linear trend line through a given set of +// y-values and (optionally), a given set of x-values. The function then +// extends the linear trendline to calculate additional y-values for a further +// supplied set of new x-values. The syntax of the function is: +// +// TREND(known_y's,[known_x's],[new_x's],[const]) +// +func (fn *formulaFuncs) TREND(argsList *list.List) formulaArg { + return fn.trendGrowth("TREND", argsList) +} + // tTest calculates the probability associated with the Student's T Test. -func tTest(bTemplin bool, pMat1, pMat2 [][]formulaArg, nC1, nC2, nR1, nR2 int, fT, fF float64) (float64, float64, bool) { - var fCount1, fCount2, fSum1, fSumSqr1, fSum2, fSumSqr2 float64 +func tTest(bTemplin bool, mtx1, mtx2 [][]formulaArg, c1, c2, r1, r2 int, fT, fF float64) (float64, float64, bool) { + var cnt1, cnt2, sum1, sumSqr1, sum2, sumSqr2 float64 var fVal formulaArg - for i := 0; i < nC1; i++ { - for j := 0; j < nR1; j++ { - fVal = pMat1[i][j].ToNumber() + for i := 0; i < c1; i++ { + for j := 0; j < r1; j++ { + fVal = mtx1[i][j].ToNumber() if fVal.Type == ArgNumber { - fSum1 += fVal.Number - fSumSqr1 += fVal.Number * fVal.Number - fCount1++ + sum1 += fVal.Number + sumSqr1 += fVal.Number * fVal.Number + cnt1++ } } } - for i := 0; i < nC2; i++ { - for j := 0; j < nR2; j++ { - fVal = pMat2[i][j].ToNumber() + for i := 0; i < c2; i++ { + for j := 0; j < r2; j++ { + fVal = mtx2[i][j].ToNumber() if fVal.Type == ArgNumber { - fSum2 += fVal.Number - fSumSqr2 += fVal.Number * fVal.Number - fCount2++ + sum2 += fVal.Number + sumSqr2 += fVal.Number * fVal.Number + cnt2++ } } } - if fCount1 < 2.0 || fCount2 < 2.0 { + if cnt1 < 2.0 || cnt2 < 2.0 { return 0, 0, false } if bTemplin { - fS1 := (fSumSqr1 - fSum1*fSum1/fCount1) / (fCount1 - 1) / fCount1 - fS2 := (fSumSqr2 - fSum2*fSum2/fCount2) / (fCount2 - 1) / fCount2 + fS1 := (sumSqr1 - sum1*sum1/cnt1) / (cnt1 - 1) / cnt1 + fS2 := (sumSqr2 - sum2*sum2/cnt2) / (cnt2 - 1) / cnt2 if fS1+fS2 == 0 { return 0, 0, false } c := fS1 / (fS1 + fS2) - fT = math.Abs(fSum1/fCount1-fSum2/fCount2) / math.Sqrt(fS1+fS2) - fF = 1 / (c*c/(fCount1-1) + (1-c)*(1-c)/(fCount2-1)) + fT = math.Abs(sum1/cnt1-sum2/cnt2) / math.Sqrt(fS1+fS2) + fF = 1 / (c*c/(cnt1-1) + (1-c)*(1-c)/(cnt2-1)) return fT, fF, true } - fS1 := (fSumSqr1 - fSum1*fSum1/fCount1) / (fCount1 - 1) - fS2 := (fSumSqr2 - fSum2*fSum2/fCount2) / (fCount2 - 1) - fT = math.Abs(fSum1/fCount1-fSum2/fCount2) / math.Sqrt((fCount1-1)*fS1+(fCount2-1)*fS2) * math.Sqrt(fCount1*fCount2*(fCount1+fCount2-2)/(fCount1+fCount2)) - fF = fCount1 + fCount2 - 2 + fS1 := (sumSqr1 - sum1*sum1/cnt1) / (cnt1 - 1) + fS2 := (sumSqr2 - sum2*sum2/cnt2) / (cnt2 - 1) + fT = math.Abs(sum1/cnt1-sum2/cnt2) / math.Sqrt((cnt1-1)*fS1+(cnt2-1)*fS2) * math.Sqrt(cnt1*cnt2*(cnt1+cnt2-2)/(cnt1+cnt2)) + fF = cnt1 + cnt2 - 2 return fT, fF, true } // tTest is an implementation of the formula function TTEST. -func (fn *formulaFuncs) tTest(pMat1, pMat2 [][]formulaArg, fTails, fTyp float64) formulaArg { +func (fn *formulaFuncs) tTest(mtx1, mtx2 [][]formulaArg, fTails, fTyp float64) formulaArg { var fT, fF float64 - nC1 := len(pMat1) - nC2 := len(pMat2) - nR1 := len(pMat1[0]) - nR2 := len(pMat2[0]) - ok := true + c1, c2, r1, r2, ok := len(mtx1), len(mtx2), len(mtx1[0]), len(mtx2[0]), true if fTyp == 1 { - if nC1 != nC2 || nR1 != nR2 { + if c1 != c2 || r1 != r2 { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - var fCount, fSum1, fSum2, fSumSqrD float64 + var cnt, sum1, sum2, sumSqrD float64 var fVal1, fVal2 formulaArg - for i := 0; i < nC1; i++ { - for j := 0; j < nR1; j++ { - fVal1 = pMat1[i][j].ToNumber() - fVal2 = pMat2[i][j].ToNumber() + for i := 0; i < c1; i++ { + for j := 0; j < r1; j++ { + fVal1, fVal2 = mtx1[i][j].ToNumber(), mtx2[i][j].ToNumber() if fVal1.Type != ArgNumber || fVal2.Type != ArgNumber { continue } - fSum1 += fVal1.Number - fSum2 += fVal2.Number - fSumSqrD += (fVal1.Number - fVal2.Number) * (fVal1.Number - fVal2.Number) - fCount++ + sum1 += fVal1.Number + sum2 += fVal2.Number + sumSqrD += (fVal1.Number - fVal2.Number) * (fVal1.Number - fVal2.Number) + cnt++ } } - if fCount < 1 { + if cnt < 1 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - fSumD := fSum1 - fSum2 - fDivider := fCount*fSumSqrD - fSumD*fSumD - if fDivider == 0 { + sumD := sum1 - sum2 + divider := cnt*sumSqrD - sumD*sumD + if divider == 0 { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - fT = math.Abs(fSumD) * math.Sqrt((fCount-1)/fDivider) - fF = fCount - 1 + fT = math.Abs(sumD) * math.Sqrt((cnt-1)/divider) + fF = cnt - 1 } else if fTyp == 2 { - fT, fF, ok = tTest(false, pMat1, pMat2, nC1, nC2, nR1, nR2, fT, fF) + fT, fF, ok = tTest(false, mtx1, mtx2, c1, c2, r1, r2, fT, fF) } else { - fT, fF, ok = tTest(true, pMat1, pMat2, nC1, nC2, nR1, nR2, fT, fF) + fT, fF, ok = tTest(true, mtx1, mtx2, c1, c2, r1, r2, fT, fF) } if !ok { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) diff --git a/calc_test.go b/calc_test.go index 6cae4a3ff7..c09174734d 100644 --- a/calc_test.go +++ b/calc_test.go @@ -572,9 +572,9 @@ func TestCalcCellValue(t *testing.T) { "=IMPRODUCT(COMPLEX(5,2),COMPLEX(0,1))": "-2+5i", "=IMPRODUCT(A1:C1)": "4", // MINVERSE - "=MINVERSE(A1:B2)": "", + "=MINVERSE(A1:B2)": "-0", // MMULT - "=MMULT(A4:A4,A4:A4)": "", + "=MMULT(A4:A4,A4:A4)": "0", // MOD "=MOD(6,4)": "2", "=MOD(6,3)": "0", @@ -597,7 +597,7 @@ func TestCalcCellValue(t *testing.T) { `=MULTINOMIAL("",3,1,2,5)`: "27720", "=MULTINOMIAL(MULTINOMIAL(1))": "1", // _xlfn.MUNIT - "=_xlfn.MUNIT(4)": "", + "=_xlfn.MUNIT(4)": "1", // ODD "=ODD(22)": "23", "=ODD(1.22)": "3", @@ -4444,6 +4444,83 @@ func TestCalcFORMULATEXT(t *testing.T) { } } +func TestCalcGROWTHandTREND(t *testing.T) { + cellData := [][]interface{}{ + {"known_x's", "known_y's", 0, -1}, + {1, 10, 1}, + {2, 20, 1}, + {3, 40}, + {4, 80}, + {}, + {"new_x's", "new_y's"}, + {5}, + {6}, + {7}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=GROWTH(A2:B2)": "1", + "=GROWTH(B2:B5,A2:A5,A8:A10)": "160", + "=GROWTH(B2:B5,A2:A5,A8:A10,FALSE)": "467.84375", + "=GROWTH(A4:A5,A2:B3,A8:A10,FALSE)": "", + "=GROWTH(A3:A5,A2:B4,A2:B3)": "2", + "=GROWTH(A4:A5,A2:B3)": "", + "=GROWTH(A2:B2,A2:B3)": "", + "=GROWTH(A2:B2,A2:B3,A2:B3,FALSE)": "1.28399658203125", + "=GROWTH(A2:B2,A4:B5,A4:B5,FALSE)": "1", + "=GROWTH(A3:C3,A2:C3,A2:B3)": "2", + "=TREND(A2:B2)": "1", + "=TREND(B2:B5,A2:A5,A8:A10)": "95", + "=TREND(B2:B5,A2:A5,A8:A10,FALSE)": "81.66796875", + "=TREND(A4:A5,A2:B3,A8:A10,FALSE)": "", + "=TREND(A4:A5,A2:B3,A2:B3,FALSE)": "1.5", + "=TREND(A3:A5,A2:B4,A2:B3)": "2", + "=TREND(A4:A5,A2:B3)": "", + "=TREND(A2:B2,A2:B3)": "", + "=TREND(A2:B2,A2:B3,A2:B3,FALSE)": "1", + "=TREND(A2:B2,A4:B5,A4:B5,FALSE)": "1", + "=TREND(A3:C3,A2:C3,A2:B3)": "2", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError := map[string]string{ + "=GROWTH()": "GROWTH requires at least 1 argument", + "=GROWTH(B2:B5,A2:A5,A8:A10,TRUE,0)": "GROWTH allows at most 4 arguments", + "=GROWTH(A1:B1,A2:A5,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", + "=GROWTH(B2:B5,A1:B1,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", + "=GROWTH(B2:B5,A2:A5,A1:B1,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", + "=GROWTH(B2:B5,A2:A5,A8:A10,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=GROWTH(A2:B3,A4:B4)": "#REF!", + "=GROWTH(A4:B4,A2:A2)": "#REF!", + "=GROWTH(A2:A2,A4:A5)": "#REF!", + "=GROWTH(C1:C1,A2:A3)": "#NUM!", + "=GROWTH(D1:D1,A2:A3)": "#NUM!", + "=GROWTH(A2:A3,C1:C1)": "#NUM!", + "=TREND()": "TREND requires at least 1 argument", + "=TREND(B2:B5,A2:A5,A8:A10,TRUE,0)": "TREND allows at most 4 arguments", + "=TREND(A1:B1,A2:A5,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", + "=TREND(B2:B5,A1:B1,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", + "=TREND(B2:B5,A2:A5,A1:B1,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", + "=TREND(B2:B5,A2:A5,A8:A10,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=TREND(A2:B3,A4:B4)": "#REF!", + "=TREND(A4:B4,A2:A2)": "#REF!", + "=TREND(A2:A2,A4:A5)": "#REF!", + "=TREND(C1:C1,A2:A3)": "#NUM!", + "=TREND(D1:D1,A2:A3)": "#REF!", + "=TREND(A2:A3,C1:C1)": "#NUM!", + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.EqualError(t, err, expected, formula) + assert.Equal(t, "", result, formula) + } +} + func TestCalcHLOOKUP(t *testing.T) { cellData := [][]interface{}{ {"Example Result Table"}, @@ -4953,7 +5030,7 @@ func TestCalcMODE(t *testing.T) { formulaList := map[string]string{ "=MODE(A1:A10)": "3", "=MODE(B1:B6)": "2", - "=MODE.MULT(A1:A10)": "", + "=MODE.MULT(A1:A10)": "3", "=MODE.SNGL(A1:A10)": "3", "=MODE.SNGL(B1:B6)": "2", } From c2be30ce90621e4473940d521995a6ce97537da6 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 19 Apr 2022 20:54:05 +0800 Subject: [PATCH 033/213] This closes #1203, supporting same field used for pivot table data and rows/cols --- pivotTable.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index 437d22f0e1..28c863290f 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -542,6 +542,7 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ Name: f.getPivotTableFieldName(name, opt.Rows), Axis: "axisRow", + DataField: inPivotTableField(opt.Data, name) != -1, Compact: &rowOptions.Compact, Outline: &rowOptions.Outline, DefaultSubtotal: &rowOptions.DefaultSubtotal, @@ -554,8 +555,9 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio } if inPivotTableField(opt.Filter, name) != -1 { pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ - Axis: "axisPage", - Name: f.getPivotTableFieldName(name, opt.Columns), + Axis: "axisPage", + DataField: inPivotTableField(opt.Data, name) != -1, + Name: f.getPivotTableFieldName(name, opt.Columns), Items: &xlsxItems{ Count: 1, Item: []*xlsxItem{ @@ -576,6 +578,7 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ Name: f.getPivotTableFieldName(name, opt.Columns), Axis: "axisCol", + DataField: inPivotTableField(opt.Data, name) != -1, Compact: &columnOptions.Compact, Outline: &columnOptions.Outline, DefaultSubtotal: &columnOptions.DefaultSubtotal, From 81d9362b4f1cf765712b61837d5b5831d1cd0c58 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 20 Apr 2022 00:01:39 +0800 Subject: [PATCH 034/213] ref #65, new formula function: CONVERT --- calc.go | 538 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 54 ++++++ 2 files changed, 592 insertions(+) diff --git a/calc.go b/calc.go index f4ab9c4a72..37f7d6c4a8 100644 --- a/calc.go +++ b/calc.go @@ -58,6 +58,20 @@ const ( criteriaErr criteriaRegexp + catgoryWeightAndMass + catgoryDistance + catgoryTime + catgoryPressure + catgoryForce + catgoryEnergy + catgoryPower + catgoryMagnetism + catgoryTemperature + catgoryVolumeAndLiquidMeasure + catgoryArea + catgoryInformation + catgorySpeed + matchModeExact = 0 matchModeMinGreater = 1 matchModeMaxLess = -1 @@ -375,6 +389,7 @@ type formulaFuncs struct { // CONFIDENCE // CONFIDENCE.NORM // CONFIDENCE.T +// CONVERT // CORREL // COS // COSH @@ -2063,6 +2078,529 @@ func str2cmplx(c string) string { return c } +// conversionUnit defined unit info for conversion. +type conversionUnit struct { + group uint8 + allowPrefix bool +} + +// conversionUnits maps info list for unit conversion, that can be used in +// formula function CONVERT. +var conversionUnits = map[string]conversionUnit{ + // weight and mass + "g": {group: catgoryWeightAndMass, allowPrefix: true}, + "sg": {group: catgoryWeightAndMass, allowPrefix: false}, + "lbm": {group: catgoryWeightAndMass, allowPrefix: false}, + "u": {group: catgoryWeightAndMass, allowPrefix: true}, + "ozm": {group: catgoryWeightAndMass, allowPrefix: false}, + "grain": {group: catgoryWeightAndMass, allowPrefix: false}, + "cwt": {group: catgoryWeightAndMass, allowPrefix: false}, + "shweight": {group: catgoryWeightAndMass, allowPrefix: false}, + "uk_cwt": {group: catgoryWeightAndMass, allowPrefix: false}, + "lcwt": {group: catgoryWeightAndMass, allowPrefix: false}, + "hweight": {group: catgoryWeightAndMass, allowPrefix: false}, + "stone": {group: catgoryWeightAndMass, allowPrefix: false}, + "ton": {group: catgoryWeightAndMass, allowPrefix: false}, + "uk_ton": {group: catgoryWeightAndMass, allowPrefix: false}, + "LTON": {group: catgoryWeightAndMass, allowPrefix: false}, + "brton": {group: catgoryWeightAndMass, allowPrefix: false}, + // distance + "m": {group: catgoryDistance, allowPrefix: true}, + "mi": {group: catgoryDistance, allowPrefix: false}, + "Nmi": {group: catgoryDistance, allowPrefix: false}, + "in": {group: catgoryDistance, allowPrefix: false}, + "ft": {group: catgoryDistance, allowPrefix: false}, + "yd": {group: catgoryDistance, allowPrefix: false}, + "ang": {group: catgoryDistance, allowPrefix: true}, + "ell": {group: catgoryDistance, allowPrefix: false}, + "ly": {group: catgoryDistance, allowPrefix: false}, + "parsec": {group: catgoryDistance, allowPrefix: false}, + "pc": {group: catgoryDistance, allowPrefix: false}, + "Pica": {group: catgoryDistance, allowPrefix: false}, + "Picapt": {group: catgoryDistance, allowPrefix: false}, + "pica": {group: catgoryDistance, allowPrefix: false}, + "survey_mi": {group: catgoryDistance, allowPrefix: false}, + // time + "yr": {group: catgoryTime, allowPrefix: false}, + "day": {group: catgoryTime, allowPrefix: false}, + "d": {group: catgoryTime, allowPrefix: false}, + "hr": {group: catgoryTime, allowPrefix: false}, + "mn": {group: catgoryTime, allowPrefix: false}, + "min": {group: catgoryTime, allowPrefix: false}, + "sec": {group: catgoryTime, allowPrefix: true}, + "s": {group: catgoryTime, allowPrefix: true}, + // pressure + "Pa": {group: catgoryPressure, allowPrefix: true}, + "p": {group: catgoryPressure, allowPrefix: true}, + "atm": {group: catgoryPressure, allowPrefix: true}, + "at": {group: catgoryPressure, allowPrefix: true}, + "mmHg": {group: catgoryPressure, allowPrefix: true}, + "psi": {group: catgoryPressure, allowPrefix: true}, + "Torr": {group: catgoryPressure, allowPrefix: true}, + // force + "N": {group: catgoryForce, allowPrefix: true}, + "dyn": {group: catgoryForce, allowPrefix: true}, + "dy": {group: catgoryForce, allowPrefix: true}, + "lbf": {group: catgoryForce, allowPrefix: false}, + "pond": {group: catgoryForce, allowPrefix: true}, + // energy + "J": {group: catgoryEnergy, allowPrefix: true}, + "e": {group: catgoryEnergy, allowPrefix: true}, + "c": {group: catgoryEnergy, allowPrefix: true}, + "cal": {group: catgoryEnergy, allowPrefix: true}, + "eV": {group: catgoryEnergy, allowPrefix: true}, + "ev": {group: catgoryEnergy, allowPrefix: true}, + "HPh": {group: catgoryEnergy, allowPrefix: false}, + "hh": {group: catgoryEnergy, allowPrefix: false}, + "Wh": {group: catgoryEnergy, allowPrefix: true}, + "wh": {group: catgoryEnergy, allowPrefix: true}, + "flb": {group: catgoryEnergy, allowPrefix: false}, + "BTU": {group: catgoryEnergy, allowPrefix: false}, + "btu": {group: catgoryEnergy, allowPrefix: false}, + // power + "HP": {group: catgoryPower, allowPrefix: false}, + "h": {group: catgoryPower, allowPrefix: false}, + "W": {group: catgoryPower, allowPrefix: true}, + "w": {group: catgoryPower, allowPrefix: true}, + "PS": {group: catgoryPower, allowPrefix: false}, + "T": {group: catgoryMagnetism, allowPrefix: true}, + "ga": {group: catgoryMagnetism, allowPrefix: true}, + // temperature + "C": {group: catgoryTemperature, allowPrefix: false}, + "cel": {group: catgoryTemperature, allowPrefix: false}, + "F": {group: catgoryTemperature, allowPrefix: false}, + "fah": {group: catgoryTemperature, allowPrefix: false}, + "K": {group: catgoryTemperature, allowPrefix: false}, + "kel": {group: catgoryTemperature, allowPrefix: false}, + "Rank": {group: catgoryTemperature, allowPrefix: false}, + "Reau": {group: catgoryTemperature, allowPrefix: false}, + // volume + "l": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, + "L": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, + "lt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, + "tsp": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "tspm": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "tbs": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "oz": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "cup": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "pt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "us_pt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "uk_pt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "qt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "uk_qt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "gal": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "uk_gal": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "ang3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, + "ang^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, + "barrel": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "bushel": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "in3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "in^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "ft3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "ft^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "ly3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "ly^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "m3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, + "m^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, + "mi3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "mi^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "yd3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "yd^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Nmi3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Nmi^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Pica3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Pica^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Picapt3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Picapt^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "GRT": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "regton": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "MTON": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + // area + "ha": {group: catgoryArea, allowPrefix: true}, + "uk_acre": {group: catgoryArea, allowPrefix: false}, + "us_acre": {group: catgoryArea, allowPrefix: false}, + "ang2": {group: catgoryArea, allowPrefix: true}, + "ang^2": {group: catgoryArea, allowPrefix: true}, + "ar": {group: catgoryArea, allowPrefix: true}, + "ft2": {group: catgoryArea, allowPrefix: false}, + "ft^2": {group: catgoryArea, allowPrefix: false}, + "in2": {group: catgoryArea, allowPrefix: false}, + "in^2": {group: catgoryArea, allowPrefix: false}, + "ly2": {group: catgoryArea, allowPrefix: false}, + "ly^2": {group: catgoryArea, allowPrefix: false}, + "m2": {group: catgoryArea, allowPrefix: true}, + "m^2": {group: catgoryArea, allowPrefix: true}, + "Morgen": {group: catgoryArea, allowPrefix: false}, + "mi2": {group: catgoryArea, allowPrefix: false}, + "mi^2": {group: catgoryArea, allowPrefix: false}, + "Nmi2": {group: catgoryArea, allowPrefix: false}, + "Nmi^2": {group: catgoryArea, allowPrefix: false}, + "Pica2": {group: catgoryArea, allowPrefix: false}, + "Pica^2": {group: catgoryArea, allowPrefix: false}, + "Picapt2": {group: catgoryArea, allowPrefix: false}, + "Picapt^2": {group: catgoryArea, allowPrefix: false}, + "yd2": {group: catgoryArea, allowPrefix: false}, + "yd^2": {group: catgoryArea, allowPrefix: false}, + // information + "byte": {group: catgoryInformation, allowPrefix: true}, + "bit": {group: catgoryInformation, allowPrefix: true}, + // speed + "m/s": {group: catgorySpeed, allowPrefix: true}, + "m/sec": {group: catgorySpeed, allowPrefix: true}, + "m/h": {group: catgorySpeed, allowPrefix: true}, + "m/hr": {group: catgorySpeed, allowPrefix: true}, + "mph": {group: catgorySpeed, allowPrefix: false}, + "admkn": {group: catgorySpeed, allowPrefix: false}, + "kn": {group: catgorySpeed, allowPrefix: false}, +} + +// unitConversions maps details of the Units of measure conversion factors, +// organised by group. +var unitConversions = map[byte]map[string]float64{ + // conversion uses gram (g) as an intermediate unit + catgoryWeightAndMass: { + "g": 1, + "sg": 6.85217658567918e-05, + "lbm": 2.20462262184878e-03, + "u": 6.02214179421676e+23, + "ozm": 3.52739619495804e-02, + "grain": 1.54323583529414e+01, + "cwt": 2.20462262184878e-05, + "shweight": 2.20462262184878e-05, + "uk_cwt": 1.96841305522212e-05, + "lcwt": 1.96841305522212e-05, + "hweight": 1.96841305522212e-05, + "stone": 1.57473044417770e-04, + "ton": 1.10231131092439e-06, + "uk_ton": 9.84206527611061e-07, + "LTON": 9.84206527611061e-07, + "brton": 9.84206527611061e-07, + }, + // conversion uses meter (m) as an intermediate unit + catgoryDistance: { + "m": 1, + "mi": 6.21371192237334e-04, + "Nmi": 5.39956803455724e-04, + "in": 3.93700787401575e+01, + "ft": 3.28083989501312e+00, + "yd": 1.09361329833771e+00, + "ang": 1.0e+10, + "ell": 8.74890638670166e-01, + "ly": 1.05700083402462e-16, + "parsec": 3.24077928966473e-17, + "pc": 3.24077928966473e-17, + "Pica": 2.83464566929134e+03, + "Picapt": 2.83464566929134e+03, + "pica": 2.36220472440945e+02, + "survey_mi": 6.21369949494950e-04, + }, + // conversion uses second (s) as an intermediate unit + catgoryTime: { + "yr": 3.16880878140289e-08, + "day": 1.15740740740741e-05, + "d": 1.15740740740741e-05, + "hr": 2.77777777777778e-04, + "mn": 1.66666666666667e-02, + "min": 1.66666666666667e-02, + "sec": 1, + "s": 1, + }, + // conversion uses Pascal (Pa) as an intermediate unit + catgoryPressure: { + "Pa": 1, + "p": 1, + "atm": 9.86923266716013e-06, + "at": 9.86923266716013e-06, + "mmHg": 7.50063755419211e-03, + "psi": 1.45037737730209e-04, + "Torr": 7.50061682704170e-03, + }, + // conversion uses Newton (N) as an intermediate unit + catgoryForce: { + "N": 1, + "dyn": 1.0e+5, + "dy": 1.0e+5, + "lbf": 2.24808923655339e-01, + "pond": 1.01971621297793e+02, + }, + // conversion uses Joule (J) as an intermediate unit + catgoryEnergy: { + "J": 1, + "e": 9.99999519343231e+06, + "c": 2.39006249473467e-01, + "cal": 2.38846190642017e-01, + "eV": 6.24145700000000e+18, + "ev": 6.24145700000000e+18, + "HPh": 3.72506430801000e-07, + "hh": 3.72506430801000e-07, + "Wh": 2.77777916238711e-04, + "wh": 2.77777916238711e-04, + "flb": 2.37304222192651e+01, + "BTU": 9.47815067349015e-04, + "btu": 9.47815067349015e-04, + }, + // conversion uses Horsepower (HP) as an intermediate unit + catgoryPower: { + "HP": 1, + "h": 1, + "W": 7.45699871582270e+02, + "w": 7.45699871582270e+02, + "PS": 1.01386966542400e+00, + }, + // conversion uses Tesla (T) as an intermediate unit + catgoryMagnetism: { + "T": 1, + "ga": 10000, + }, + // conversion uses litre (l) as an intermediate unit + catgoryVolumeAndLiquidMeasure: { + "l": 1, + "L": 1, + "lt": 1, + "tsp": 2.02884136211058e+02, + "tspm": 2.0e+02, + "tbs": 6.76280454036860e+01, + "oz": 3.38140227018430e+01, + "cup": 4.22675283773038e+00, + "pt": 2.11337641886519e+00, + "us_pt": 2.11337641886519e+00, + "uk_pt": 1.75975398639270e+00, + "qt": 1.05668820943259e+00, + "uk_qt": 8.79876993196351e-01, + "gal": 2.64172052358148e-01, + "uk_gal": 2.19969248299088e-01, + "ang3": 1.0e+27, + "ang^3": 1.0e+27, + "barrel": 6.28981077043211e-03, + "bushel": 2.83775932584017e-02, + "in3": 6.10237440947323e+01, + "in^3": 6.10237440947323e+01, + "ft3": 3.53146667214886e-02, + "ft^3": 3.53146667214886e-02, + "ly3": 1.18093498844171e-51, + "ly^3": 1.18093498844171e-51, + "m3": 1.0e-03, + "m^3": 1.0e-03, + "mi3": 2.39912758578928e-13, + "mi^3": 2.39912758578928e-13, + "yd3": 1.30795061931439e-03, + "yd^3": 1.30795061931439e-03, + "Nmi3": 1.57426214685811e-13, + "Nmi^3": 1.57426214685811e-13, + "Pica3": 2.27769904358706e+07, + "Pica^3": 2.27769904358706e+07, + "Picapt3": 2.27769904358706e+07, + "Picapt^3": 2.27769904358706e+07, + "GRT": 3.53146667214886e-04, + "regton": 3.53146667214886e-04, + "MTON": 8.82866668037215e-04, + }, + // conversion uses hectare (ha) as an intermediate unit + catgoryArea: { + "ha": 1, + "uk_acre": 2.47105381467165e+00, + "us_acre": 2.47104393046628e+00, + "ang2": 1.0e+24, + "ang^2": 1.0e+24, + "ar": 1.0e+02, + "ft2": 1.07639104167097e+05, + "ft^2": 1.07639104167097e+05, + "in2": 1.55000310000620e+07, + "in^2": 1.55000310000620e+07, + "ly2": 1.11725076312873e-28, + "ly^2": 1.11725076312873e-28, + "m2": 1.0e+04, + "m^2": 1.0e+04, + "Morgen": 4.0e+00, + "mi2": 3.86102158542446e-03, + "mi^2": 3.86102158542446e-03, + "Nmi2": 2.91553349598123e-03, + "Nmi^2": 2.91553349598123e-03, + "Pica2": 8.03521607043214e+10, + "Pica^2": 8.03521607043214e+10, + "Picapt2": 8.03521607043214e+10, + "Picapt^2": 8.03521607043214e+10, + "yd2": 1.19599004630108e+04, + "yd^2": 1.19599004630108e+04, + }, + // conversion uses bit (bit) as an intermediate unit + catgoryInformation: { + "bit": 1, + "byte": 0.125, + }, + // conversion uses Meters per Second (m/s) as an intermediate unit + catgorySpeed: { + "m/s": 1, + "m/sec": 1, + "m/h": 3.60e+03, + "m/hr": 3.60e+03, + "mph": 2.23693629205440e+00, + "admkn": 1.94260256941567e+00, + "kn": 1.94384449244060e+00, + }, +} + +// conversionMultipliers maps details of the Multiplier prefixes that can be +// used with Units of Measure in CONVERT. +var conversionMultipliers = map[string]float64{ + "Y": 1e24, + "Z": 1e21, + "E": 1e18, + "P": 1e15, + "T": 1e12, + "G": 1e9, + "M": 1e6, + "k": 1e3, + "h": 1e2, + "e": 1e1, + "da": 1e1, + "d": 1e-1, + "c": 1e-2, + "m": 1e-3, + "u": 1e-6, + "n": 1e-9, + "p": 1e-12, + "f": 1e-15, + "a": 1e-18, + "z": 1e-21, + "y": 1e-24, + "Yi": math.Pow(2, 80), + "Zi": math.Pow(2, 70), + "Ei": math.Pow(2, 60), + "Pi": math.Pow(2, 50), + "Ti": math.Pow(2, 40), + "Gi": math.Pow(2, 30), + "Mi": math.Pow(2, 20), + "ki": math.Pow(2, 10), +} + +// getUnitDetails check and returns the unit of measure details. +func getUnitDetails(uom string) (unit string, catgory byte, res float64, ok bool) { + if len(uom) == 0 { + ok = false + return + } + if unit, ok := conversionUnits[uom]; ok { + return uom, unit.group, 1, ok + } + // 1 character standard metric multiplier prefixes + multiplierType := uom[:1] + uom = uom[1:] + conversionUnit, ok1 := conversionUnits[uom] + multiplier, ok2 := conversionMultipliers[multiplierType] + if ok1 && ok2 { + if !conversionUnit.allowPrefix { + ok = false + return + } + unitCategory := conversionUnit.group + return uom, unitCategory, multiplier, true + } + // 2 character standard and binary metric multiplier prefixes + if len(uom) > 0 { + multiplierType += uom[:1] + uom = uom[1:] + } + conversionUnit, ok1 = conversionUnits[uom] + multiplier, ok2 = conversionMultipliers[multiplierType] + if ok1 && ok2 { + if !conversionUnit.allowPrefix { + ok = false + return + } + unitCategory := conversionUnit.group + return uom, unitCategory, multiplier, true + } + ok = false + return +} + +// resolveTemperatureSynonyms returns unit of measure according to a given +// temperature synonyms. +func resolveTemperatureSynonyms(uom string) string { + switch uom { + case "fah": + return "F" + case "cel": + return "C" + case "kel": + return "K" + } + return uom +} + +// convertTemperature returns converted temperature by a given unit of measure. +func convertTemperature(fromUOM, toUOM string, value float64) float64 { + fromUOM = resolveTemperatureSynonyms(fromUOM) + toUOM = resolveTemperatureSynonyms(toUOM) + if fromUOM == toUOM { + return value + } + // convert to Kelvin + switch fromUOM { + case "F": + value = (value-32)/1.8 + 273.15 + break + case "C": + value += 273.15 + break + case "Rank": + value /= 1.8 + break + case "Reau": + value = value*1.25 + 273.15 + break + } + // convert from Kelvin + switch toUOM { + case "F": + value = (value-273.15)*1.8 + 32 + break + case "C": + value -= 273.15 + break + case "Rank": + value *= 1.8 + break + case "Reau": + value = (value - 273.15) * 0.8 + break + } + return value +} + +// CONVERT function converts a number from one unit type (e.g. Yards) to +// another unit type (e.g. Meters). The syntax of the function is: +// +// CONVERT(number,from_unit,to_unit) +// +func (fn *formulaFuncs) CONVERT(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "CONVERT requires 3 arguments") + } + num := argsList.Front().Value.(formulaArg).ToNumber() + if num.Type != ArgNumber { + return num + } + fromUOM, fromCategory, fromMultiplier, ok1 := getUnitDetails(argsList.Front().Next().Value.(formulaArg).Value()) + toUOM, toCategory, toMultiplier, ok2 := getUnitDetails(argsList.Back().Value.(formulaArg).Value()) + if !ok1 || !ok2 || fromCategory != toCategory { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + val := num.Number * fromMultiplier + if fromUOM == toUOM && fromMultiplier == toMultiplier { + return newNumberFormulaArg(val / fromMultiplier) + } else if fromUOM == toUOM { + return newNumberFormulaArg(val / toMultiplier) + } else if fromCategory == catgoryTemperature { + return newNumberFormulaArg(convertTemperature(fromUOM, toUOM, val)) + } + fromConversion, _ := unitConversions[fromCategory][fromUOM] + toConversion, _ := unitConversions[fromCategory][toUOM] + baseValue := val * (1 / fromConversion) + return newNumberFormulaArg((baseValue * toConversion) / toMultiplier) +} + // DEC2BIN function converts a decimal number into a Binary (Base 2) number. // The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index c09174734d..6d8336293e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -120,6 +120,45 @@ func TestCalcCellValue(t *testing.T) { "=COMPLEX(0,-2)": "-2i", "=COMPLEX(0,0)": "0", "=COMPLEX(0,-1,\"j\")": "-j", + // CONVERT + "=CONVERT(20.2,\"m\",\"yd\")": "22.0909886264217", + "=CONVERT(20.2,\"cm\",\"yd\")": "0.220909886264217", + "=CONVERT(0.2,\"gal\",\"tsp\")": "153.6", + "=CONVERT(5,\"gal\",\"l\")": "18.92705892", + "=CONVERT(0.02,\"Gm\",\"m\")": "20000000", + "=CONVERT(0,\"C\",\"F\")": "32", + "=CONVERT(1,\"ly^2\",\"ly^2\")": "1", + "=CONVERT(0.00194255938572296,\"sg\",\"ozm\")": "1", + "=CONVERT(5,\"kg\",\"kg\")": "5", + "=CONVERT(4.5359237E-01,\"kg\",\"lbm\")": "1", + "=CONVERT(0.2,\"kg\",\"hg\")": "2", + "=CONVERT(12.345000000000001,\"km\",\"m\")": "12345", + "=CONVERT(12345,\"m\",\"km\")": "12.345", + "=CONVERT(0.621371192237334,\"mi\",\"km\")": "1", + "=CONVERT(1.23450000000000E+05,\"ang\",\"um\")": "12.345", + "=CONVERT(1.23450000000000E+02,\"kang\",\"um\")": "12.345", + "=CONVERT(1000,\"dal\",\"hl\")": "100", + "=CONVERT(1,\"yd\",\"ft\")": "2.99999999999999", + "=CONVERT(20,\"C\",\"F\")": "68", + "=CONVERT(68,\"F\",\"C\")": "20", + "=CONVERT(293.15,\"K\",\"F\")": "68", + "=CONVERT(68,\"F\",\"K\")": "293.15", + "=CONVERT(-273.15,\"C\",\"K\")": "0", + "=CONVERT(-459.67,\"F\",\"K\")": "0", + "=CONVERT(295.65,\"K\",\"C\")": "22.5", + "=CONVERT(22.5,\"C\",\"K\")": "295.65", + "=CONVERT(1667.85,\"C\",\"K\")": "1941", + "=CONVERT(3034.13,\"F\",\"K\")": "1941", + "=CONVERT(3493.8,\"Rank\",\"K\")": "1941", + "=CONVERT(1334.28,\"Reau\",\"K\")": "1941", + "=CONVERT(1941,\"K\",\"Rank\")": "3493.8", + "=CONVERT(1941,\"K\",\"Reau\")": "1334.28", + "=CONVERT(123.45,\"K\",\"kel\")": "123.45", + "=CONVERT(123.45,\"C\",\"cel\")": "123.45", + "=CONVERT(123.45,\"F\",\"fah\")": "123.45", + "=CONVERT(16,\"bit\",\"byte\")": "2", + "=CONVERT(1,\"kbyte\",\"byte\")": "1000", + "=CONVERT(1,\"kibyte\",\"byte\")": "1024", // DEC2BIN "=DEC2BIN(2)": "10", "=DEC2BIN(3)": "11", @@ -2014,6 +2053,21 @@ func TestCalcCellValue(t *testing.T) { "=COMPLEX(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", "=COMPLEX(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=COMPLEX(10,-5,\"i\",0)": "COMPLEX allows at most 3 arguments", + // CONVERT + "=CONVERT()": "CONVERT requires 3 arguments", + "=CONVERT(\"\",\"m\",\"yd\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CONVERT(20.2,\"m\",\"C\")": "#N/A", + "=CONVERT(20.2,\"\",\"C\")": "#N/A", + "=CONVERT(100,\"dapt\",\"pt\")": "#N/A", + "=CONVERT(1,\"ft\",\"day\")": "#N/A", + "=CONVERT(234.56,\"kpt\",\"lt\")": "#N/A", + "=CONVERT(234.56,\"lt\",\"kpt\")": "#N/A", + "=CONVERT(234.56,\"kiqt\",\"pt\")": "#N/A", + "=CONVERT(234.56,\"pt\",\"kiqt\")": "#N/A", + "=CONVERT(12345.6,\"baton\",\"cwt\")": "#N/A", + "=CONVERT(12345.6,\"cwt\",\"baton\")": "#N/A", + "=CONVERT(234.56,\"xxxx\",\"m\")": "#N/A", + "=CONVERT(234.56,\"m\",\"xxxx\")": "#N/A", // DEC2BIN "=DEC2BIN()": "DEC2BIN requires at least 1 argument", "=DEC2BIN(1,1,1)": "DEC2BIN allows at most 2 arguments", From 0f7a0c8f3b5c9abd5858cab80902296d1639625f Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 24 Apr 2022 23:43:19 +0800 Subject: [PATCH 035/213] Optimization formula calculation performance and update README card badge --- README.md | 2 +- README_zh.md | 2 +- calc.go | 459 ++++++++++++++++++++++----------------------------- calc_test.go | 125 +++++++------- 4 files changed, 261 insertions(+), 327 deletions(-) diff --git a/README.md b/README.md index 8e16a88b03..89d2d001e3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

Build Status Code Coverage - Go Report Card + Go Report Card go.dev Licenses Donate diff --git a/README_zh.md b/README_zh.md index dafdd93f1e..d67b63cb03 100644 --- a/README_zh.md +++ b/README_zh.md @@ -3,7 +3,7 @@

Build Status Code Coverage - Go Report Card + Go Report Card go.dev Licenses Donate diff --git a/calc.go b/calc.go index 37f7d6c4a8..16d183b69c 100644 --- a/calc.go +++ b/calc.go @@ -736,7 +736,7 @@ type formulaFuncs struct { func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { var ( formula string - token efp.Token + token formulaArg ) if formula, err = f.GetCellFormula(sheet, cell); err != nil { return @@ -749,7 +749,7 @@ func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { if token, err = f.evalInfixExp(sheet, cell, tokens); err != nil { return } - result = token.TValue + result = token.Value() isNum, precision := isNumeric(result) if isNum && (precision > 15 || precision == 0) { num := roundPrecision(result, -1) @@ -826,7 +826,7 @@ func newEmptyFormulaArg() formulaArg { // // TODO: handle subtypes: Nothing, Text, Logical, Error, Concatenation, Intersection, Union // -func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, error) { +func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, error) { var err error opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack() for i := 0; i < len(tokens); i++ { @@ -835,7 +835,7 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, // out of function stack if opfStack.Len() == 0 { if err = f.parseToken(sheet, token, opdStack, optStack); err != nil { - return efp.Token{}, err + return newEmptyFormulaArg(), err } } @@ -864,16 +864,12 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, // parse reference: must reference at here result, err := f.parseReference(sheet, token.TValue) if err != nil { - return efp.Token{TValue: formulaErrorNAME}, err + return result, err } if result.Type != ArgString { - return efp.Token{}, errors.New(formulaErrorVALUE) + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE), errors.New(formulaErrorVALUE) } - opfdStack.Push(efp.Token{ - TType: efp.TokenTypeOperand, - TSubType: efp.TokenSubTypeNumber, - TValue: result.String, - }) + opfdStack.Push(result) continue } if nextToken.TType == efp.TokenTypeArgument || nextToken.TType == efp.TokenTypeFunction { @@ -884,10 +880,10 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, } result, err := f.parseReference(sheet, token.TValue) if err != nil { - return efp.Token{TValue: formulaErrorNAME}, err + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE), err } if result.Type == ArgUnknown { - return efp.Token{}, errors.New(formulaErrorVALUE) + return newEmptyFormulaArg(), errors.New(formulaErrorVALUE) } argsStack.Peek().(*list.List).PushBack(result) continue @@ -896,18 +892,14 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, if isEndParenthesesToken(token) && isBeginParenthesesToken(opftStack.Peek().(efp.Token)) { if arg := argsStack.Peek().(*list.List).Back(); arg != nil { - opfdStack.Push(efp.Token{ - TType: efp.TokenTypeOperand, - TSubType: efp.TokenSubTypeNumber, - TValue: arg.Value.(formulaArg).Value(), - }) + opfdStack.Push(arg.Value.(formulaArg)) argsStack.Peek().(*list.List).Remove(arg) } } // check current token is opft if err = f.parseToken(sheet, token, opfdStack, opftStack); err != nil { - return efp.Token{}, err + return newEmptyFormulaArg(), err } // current token is arg @@ -921,7 +913,7 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, opftStack.Pop() } if !opfdStack.Empty() { - argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(opfdStack.Pop().(efp.Token).TValue)) + argsStack.Peek().(*list.List).PushBack(opfdStack.Pop().(formulaArg)) } continue } @@ -932,21 +924,21 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, } if err = f.evalInfixExpFunc(sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { - return efp.Token{}, err + return newEmptyFormulaArg(), err } } } for optStack.Len() != 0 { topOpt := optStack.Peek().(efp.Token) if err = calculate(opdStack, topOpt); err != nil { - return efp.Token{}, err + return newEmptyFormulaArg(), err } optStack.Pop() } if opdStack.Len() == 0 { - return efp.Token{}, ErrInvalidFormula + return newEmptyFormulaArg(), ErrInvalidFormula } - return opdStack.Peek().(efp.Token), err + return opdStack.Peek().(formulaArg), err } // evalInfixExpFunc evaluate formula function in the infix expression. @@ -968,11 +960,7 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, if opfStack.Len() > 0 { // still in function stack if nextToken.TType == efp.TokenTypeOperatorInfix || (opftStack.Len() > 1 && opfdStack.Len() > 0) { // mathematics calculate in formula function - if arg.Type == ArgError { - opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeError}) - } else { - opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) - } + opfdStack.Push(arg) } else { argsStack.Peek().(*list.List).PushBack(arg) } @@ -981,7 +969,7 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, if arg.Type == ArgMatrix && len(arg.Matrix) > 0 && len(arg.Matrix[0]) > 0 { val = arg.Matrix[0][0].Value() } - opdStack.Push(efp.Token{TValue: val, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(newStringFormulaArg(val)) } return nil } @@ -1010,179 +998,166 @@ func prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack *Stack) { } // push opfd to args if argument && opfdStack.Len() > 0 { - argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(opfdStack.Pop().(efp.Token).TValue)) + argsStack.Peek().(*list.List).PushBack(opfdStack.Pop().(formulaArg)) } } // calcPow evaluate exponentiation arithmetic operations. -func calcPow(rOpd, lOpd efp.Token, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { - return err +func calcPow(rOpd, lOpd formulaArg, opdStack *Stack) error { + lOpdVal := lOpd.ToNumber() + if lOpdVal.Type != ArgNumber { + return errors.New(lOpdVal.Value()) } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err + rOpdVal := rOpd.ToNumber() + if rOpdVal.Type != ArgNumber { + return errors.New(rOpdVal.Value()) } - result := math.Pow(lOpdVal, rOpdVal) - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(newNumberFormulaArg(math.Pow(lOpdVal.Number, rOpdVal.Number))) return nil } // calcEq evaluate equal arithmetic operations. -func calcEq(rOpd, lOpd efp.Token, opdStack *Stack) error { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpd.TValue == lOpd.TValue)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +func calcEq(rOpd, lOpd formulaArg, opdStack *Stack) error { + opdStack.Push(newBoolFormulaArg(rOpd.Value() == lOpd.Value())) return nil } // calcNEq evaluate not equal arithmetic operations. -func calcNEq(rOpd, lOpd efp.Token, opdStack *Stack) error { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpd.TValue != lOpd.TValue)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +func calcNEq(rOpd, lOpd formulaArg, opdStack *Stack) error { + opdStack.Push(newBoolFormulaArg(rOpd.Value() != lOpd.Value())) return nil } // calcL evaluate less than arithmetic operations. -func calcL(rOpd, lOpd efp.Token, opdStack *Stack) error { - if rOpd.TSubType == efp.TokenSubTypeNumber && lOpd.TSubType == efp.TokenSubTypeNumber { - lOpdVal, _ := strconv.ParseFloat(lOpd.TValue, 64) - rOpdVal, _ := strconv.ParseFloat(rOpd.TValue, 64) - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(lOpdVal < rOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +func calcL(rOpd, lOpd formulaArg, opdStack *Stack) error { + if rOpd.Type == ArgNumber && lOpd.Type == ArgNumber { + opdStack.Push(newBoolFormulaArg(lOpd.Number < rOpd.Number)) } - if rOpd.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeText { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd.TValue, rOpd.TValue) == -1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgString && lOpd.Type == ArgString { + opdStack.Push(newBoolFormulaArg(strings.Compare(lOpd.Value(), rOpd.Value()) == -1)) } - if rOpd.TSubType == efp.TokenSubTypeNumber && lOpd.TSubType == efp.TokenSubTypeText { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(false)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgNumber && lOpd.Type == ArgString { + opdStack.Push(newBoolFormulaArg(false)) } - if rOpd.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeNumber { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(true)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgString && lOpd.Type == ArgNumber { + opdStack.Push(newBoolFormulaArg(true)) } return nil } // calcLe evaluate less than or equal arithmetic operations. -func calcLe(rOpd, lOpd efp.Token, opdStack *Stack) error { - if rOpd.TSubType == efp.TokenSubTypeNumber && lOpd.TSubType == efp.TokenSubTypeNumber { - lOpdVal, _ := strconv.ParseFloat(lOpd.TValue, 64) - rOpdVal, _ := strconv.ParseFloat(rOpd.TValue, 64) - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(lOpdVal <= rOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +func calcLe(rOpd, lOpd formulaArg, opdStack *Stack) error { + if rOpd.Type == ArgNumber && lOpd.Type == ArgNumber { + opdStack.Push(newBoolFormulaArg(lOpd.Number <= rOpd.Number)) } - if rOpd.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeText { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd.TValue, rOpd.TValue) != 1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgString && lOpd.Type == ArgString { + opdStack.Push(newBoolFormulaArg(strings.Compare(lOpd.Value(), rOpd.Value()) != 1)) } - if rOpd.TSubType == efp.TokenSubTypeNumber && lOpd.TSubType == efp.TokenSubTypeText { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(false)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgNumber && lOpd.Type == ArgString { + opdStack.Push(newBoolFormulaArg(false)) } - if rOpd.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeNumber { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(true)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgString && lOpd.Type == ArgNumber { + opdStack.Push(newBoolFormulaArg(true)) } return nil } // calcG evaluate greater than or equal arithmetic operations. -func calcG(rOpd, lOpd efp.Token, opdStack *Stack) error { - if rOpd.TSubType == efp.TokenSubTypeNumber && lOpd.TSubType == efp.TokenSubTypeNumber { - lOpdVal, _ := strconv.ParseFloat(lOpd.TValue, 64) - rOpdVal, _ := strconv.ParseFloat(rOpd.TValue, 64) - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(lOpdVal > rOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +func calcG(rOpd, lOpd formulaArg, opdStack *Stack) error { + if rOpd.Type == ArgNumber && lOpd.Type == ArgNumber { + opdStack.Push(newBoolFormulaArg(lOpd.Number > rOpd.Number)) } - if rOpd.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeText { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd.TValue, rOpd.TValue) == 1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgString && lOpd.Type == ArgString { + opdStack.Push(newBoolFormulaArg(strings.Compare(lOpd.Value(), rOpd.Value()) == 1)) } - if rOpd.TSubType == efp.TokenSubTypeNumber && lOpd.TSubType == efp.TokenSubTypeText { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(true)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgNumber && lOpd.Type == ArgString { + opdStack.Push(newBoolFormulaArg(true)) } - if rOpd.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeNumber { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(false)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgString && lOpd.Type == ArgNumber { + opdStack.Push(newBoolFormulaArg(false)) } return nil } // calcGe evaluate greater than or equal arithmetic operations. -func calcGe(rOpd, lOpd efp.Token, opdStack *Stack) error { - if rOpd.TSubType == efp.TokenSubTypeNumber && lOpd.TSubType == efp.TokenSubTypeNumber { - lOpdVal, _ := strconv.ParseFloat(lOpd.TValue, 64) - rOpdVal, _ := strconv.ParseFloat(rOpd.TValue, 64) - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(lOpdVal >= rOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +func calcGe(rOpd, lOpd formulaArg, opdStack *Stack) error { + if rOpd.Type == ArgNumber && lOpd.Type == ArgNumber { + opdStack.Push(newBoolFormulaArg(lOpd.Number >= rOpd.Number)) } - if rOpd.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeText { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd.TValue, rOpd.TValue) != -1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgString && lOpd.Type == ArgString { + opdStack.Push(newBoolFormulaArg(strings.Compare(lOpd.Value(), rOpd.Value()) != -1)) } - if rOpd.TSubType == efp.TokenSubTypeNumber && lOpd.TSubType == efp.TokenSubTypeText { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(true)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgNumber && lOpd.Type == ArgString { + opdStack.Push(newBoolFormulaArg(true)) } - if rOpd.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeNumber { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(false)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if rOpd.Type == ArgString && lOpd.Type == ArgNumber { + opdStack.Push(newBoolFormulaArg(false)) } return nil } // calcSplice evaluate splice '&' operations. -func calcSplice(rOpd, lOpd efp.Token, opdStack *Stack) error { - opdStack.Push(efp.Token{TValue: lOpd.TValue + rOpd.TValue, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +func calcSplice(rOpd, lOpd formulaArg, opdStack *Stack) error { + opdStack.Push(newStringFormulaArg(lOpd.Value() + rOpd.Value())) return nil } // calcAdd evaluate addition arithmetic operations. -func calcAdd(rOpd, lOpd efp.Token, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { - return err +func calcAdd(rOpd, lOpd formulaArg, opdStack *Stack) error { + lOpdVal := lOpd.ToNumber() + if lOpdVal.Type != ArgNumber { + return errors.New(lOpdVal.Value()) } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err + rOpdVal := rOpd.ToNumber() + if rOpdVal.Type != ArgNumber { + return errors.New(rOpdVal.Value()) } - result := lOpdVal + rOpdVal - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(newNumberFormulaArg(lOpdVal.Number + rOpdVal.Number)) return nil } // calcSubtract evaluate subtraction arithmetic operations. -func calcSubtract(rOpd, lOpd efp.Token, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { - return err +func calcSubtract(rOpd, lOpd formulaArg, opdStack *Stack) error { + lOpdVal := lOpd.ToNumber() + if lOpdVal.Type != ArgNumber { + return errors.New(lOpdVal.Value()) } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err + rOpdVal := rOpd.ToNumber() + if rOpdVal.Type != ArgNumber { + return errors.New(rOpdVal.Value()) } - result := lOpdVal - rOpdVal - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(newNumberFormulaArg(lOpdVal.Number - rOpdVal.Number)) return nil } // calcMultiply evaluate multiplication arithmetic operations. -func calcMultiply(rOpd, lOpd efp.Token, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { - return err +func calcMultiply(rOpd, lOpd formulaArg, opdStack *Stack) error { + lOpdVal := lOpd.ToNumber() + if lOpdVal.Type != ArgNumber { + return errors.New(lOpdVal.Value()) } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err + rOpdVal := rOpd.ToNumber() + if rOpdVal.Type != ArgNumber { + return errors.New(rOpdVal.Value()) } - result := lOpdVal * rOpdVal - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(newNumberFormulaArg(lOpdVal.Number * rOpdVal.Number)) return nil } // calcDiv evaluate division arithmetic operations. -func calcDiv(rOpd, lOpd efp.Token, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { - return err +func calcDiv(rOpd, lOpd formulaArg, opdStack *Stack) error { + lOpdVal := lOpd.ToNumber() + if lOpdVal.Type != ArgNumber { + return errors.New(lOpdVal.Value()) } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err + rOpdVal := rOpd.ToNumber() + if rOpdVal.Type != ArgNumber { + return errors.New(rOpdVal.Value()) } - result := lOpdVal / rOpdVal - if rOpdVal == 0 { + if rOpdVal.Number == 0 { return errors.New(formulaErrorDIV) } - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(newNumberFormulaArg(lOpdVal.Number / rOpdVal.Number)) return nil } @@ -1192,25 +1167,20 @@ func calculate(opdStack *Stack, opt efp.Token) error { if opdStack.Len() < 1 { return ErrInvalidFormula } - opd := opdStack.Pop().(efp.Token) - opdVal, err := strconv.ParseFloat(opd.TValue, 64) - if err != nil { - return err - } - result := 0 - opdVal - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opd := opdStack.Pop().(formulaArg) + opdStack.Push(newNumberFormulaArg(0 - opd.Number)) } if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { if opdStack.Len() < 2 { return ErrInvalidFormula } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) + rOpd := opdStack.Pop().(formulaArg) + lOpd := opdStack.Pop().(formulaArg) if err := calcSubtract(rOpd, lOpd, opdStack); err != nil { return err } } - tokenCalcFunc := map[string]func(rOpd, lOpd efp.Token, opdStack *Stack) error{ + tokenCalcFunc := map[string]func(rOpd, lOpd formulaArg, opdStack *Stack) error{ "^": calcPow, "*": calcMultiply, "/": calcDiv, @@ -1228,13 +1198,13 @@ func calculate(opdStack *Stack, opt efp.Token) error { if opdStack.Len() < 2 { return ErrInvalidFormula } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - if rOpd.TSubType == efp.TokenSubTypeError { - return errors.New(rOpd.TValue) + rOpd := opdStack.Pop().(formulaArg) + lOpd := opdStack.Pop().(formulaArg) + if rOpd.Type == ArgError { + return errors.New(rOpd.Value()) } - if lOpd.TSubType == efp.TokenSubTypeError { - return errors.New(lOpd.TValue) + if lOpd.Type == ArgError { + return errors.New(lOpd.Value()) } if err := fn(rOpd, lOpd, opdStack); err != nil { return err @@ -1322,7 +1292,7 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta } token.TValue = result.String token.TType = efp.TokenTypeOperand - token.TSubType = efp.TokenSubTypeNumber + token.TSubType = efp.TokenSubTypeText } if isOperatorPrefixToken(token) { if err := f.parseOperatorPrefixToken(optStack, opdStack, token); err != nil { @@ -1343,15 +1313,17 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta optStack.Pop() } if token.TType == efp.TokenTypeOperatorPostfix && !opdStack.Empty() { - topOpd := opdStack.Pop().(efp.Token) - opd, err := strconv.ParseFloat(topOpd.TValue, 64) - topOpd.TValue = strconv.FormatFloat(opd/100, 'f', -1, 64) - opdStack.Push(topOpd) - return err + topOpd := opdStack.Pop().(formulaArg) + opdStack.Push(newNumberFormulaArg(topOpd.Number / 100)) } // opd if isOperand(token) { - opdStack.Push(token) + if token.TSubType == efp.TokenSubTypeNumber { + num, _ := strconv.ParseFloat(token.TValue, 64) + opdStack.Push(newNumberFormulaArg(num)) + } else { + opdStack.Push(newStringFormulaArg(token.TValue)) + } } return nil } @@ -3723,7 +3695,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "radix must be an integer >= 2 and <= 36") } if argsList.Len() > 2 { - if minLength, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil { + if minLength, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value()); err != nil { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } } @@ -4058,17 +4030,16 @@ func (fn *formulaFuncs) DECIMAL(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "DECIMAL requires 2 numeric arguments") } - text := argsList.Front().Value.(formulaArg).String - var radix int + text := argsList.Front().Value.(formulaArg).Value() var err error - radix, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + radix := argsList.Back().Value.(formulaArg).ToNumber() + if radix.Type != ArgNumber { + return radix } if len(text) > 2 && (strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X")) { text = text[2:] } - val, err := strconv.ParseInt(text, radix, 64) + val, err := strconv.ParseInt(text, int(radix.Number), 64) if err != nil { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } @@ -4948,8 +4919,6 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) formulaArg { for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { - case ArgUnknown: - continue case ArgString: if token.String == "" { continue @@ -4963,13 +4932,13 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) formulaArg { case ArgMatrix: for _, row := range token.Matrix { for _, value := range row { - if value.String == "" { + if value.Value() == "" { continue } if val, err = strconv.ParseFloat(value.String, 64); err != nil { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - product = product * val + product *= val } } } @@ -5684,10 +5653,9 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { ok, _ = formulaCriteriaEval(fromVal, criteria) if ok { if argsList.Len() == 3 { - if len(sumRange) <= rowIdx || len(sumRange[rowIdx]) <= colIdx { - continue + if len(sumRange) > rowIdx && len(sumRange[rowIdx]) > colIdx { + fromVal = sumRange[rowIdx][colIdx].String } - fromVal = sumRange[rowIdx][colIdx].String } if val, err = strconv.ParseFloat(fromVal, 64); err != nil { continue @@ -5718,6 +5686,9 @@ func (fn *formulaFuncs) SUMIFS(argsList *list.List) formulaArg { args = append(args, arg.Value.(formulaArg)) } for _, ref := range formulaIfsMatch(args) { + if ref.Row >= len(sumRange) || ref.Col >= len(sumRange[ref.Row]) { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } if num := sumRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber { sum += num.Number } @@ -5812,14 +5783,14 @@ func (fn *formulaFuncs) SUMSQ(argsList *list.List) formulaArg { } sq += val * val case ArgNumber: - sq += token.Number + sq += token.Number * token.Number case ArgMatrix: for _, row := range token.Matrix { for _, value := range row { - if value.String == "" { + if value.Value() == "" { continue } - if val, err = strconv.ParseFloat(value.String, 64); err != nil { + if val, err = strconv.ParseFloat(value.Value(), 64); err != nil { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } sq += val * val @@ -6028,7 +5999,7 @@ func (fn *formulaFuncs) AVERAGEIF(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "AVERAGEIF requires at least 2 arguments") } var ( - criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) + criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).Value()) rangeMtx = argsList.Front().Value.(formulaArg).Matrix cellRange [][]formulaArg args []formulaArg @@ -6041,17 +6012,16 @@ func (fn *formulaFuncs) AVERAGEIF(argsList *list.List) formulaArg { } for rowIdx, row := range rangeMtx { for colIdx, col := range row { - fromVal := col.String - if col.String == "" { + fromVal := col.Value() + if col.Value() == "" { continue } ok, _ = formulaCriteriaEval(fromVal, criteria) if ok { if argsList.Len() == 3 { - if len(cellRange) <= rowIdx || len(cellRange[rowIdx]) <= colIdx { - continue + if len(cellRange) > rowIdx && len(cellRange[rowIdx]) > colIdx { + fromVal = cellRange[rowIdx][colIdx].Value() } - fromVal = cellRange[rowIdx][colIdx].String } if val, err = strconv.ParseFloat(fromVal, 64); err != nil { continue @@ -7686,12 +7656,10 @@ func (fn *formulaFuncs) COUNT(argsList *list.List) formulaArg { for token := argsList.Front(); token != nil; token = token.Next() { arg := token.Value.(formulaArg) switch arg.Type { - case ArgString: + case ArgString, ArgNumber: if arg.ToNumber().Type != ArgError { count++ } - case ArgNumber: - count++ case ArgMatrix: for _, row := range arg.Matrix { for _, value := range row { @@ -7928,23 +7896,14 @@ func (fn *formulaFuncs) GAMMA(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "GAMMA requires 1 numeric argument") } - token := argsList.Front().Value.(formulaArg) - switch token.Type { - case ArgString: - arg := token.ToNumber() - if arg.Type == ArgNumber { - if arg.Number <= 0 { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) - } - return newNumberFormulaArg(math.Gamma(arg.Number)) - } - case ArgNumber: - if token.Number <= 0 { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) - } - return newNumberFormulaArg(math.Gamma(token.Number)) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, "GAMMA requires 1 numeric argument") + } + if number.Number <= 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - return newErrorFormulaArg(formulaErrorVALUE, "GAMMA requires 1 numeric argument") + return newNumberFormulaArg(math.Gamma(number.Number)) } // GAMMAdotDIST function returns the Gamma Distribution, which is frequently @@ -8073,23 +8032,14 @@ func (fn *formulaFuncs) GAMMALN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") } - token := argsList.Front().Value.(formulaArg) - switch token.Type { - case ArgString: - arg := token.ToNumber() - if arg.Type == ArgNumber { - if arg.Number <= 0 { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) - } - return newNumberFormulaArg(math.Log(math.Gamma(arg.Number))) - } - case ArgNumber: - if token.Number <= 0 { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) - } - return newNumberFormulaArg(math.Log(math.Gamma(token.Number))) + x := argsList.Front().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") + } + if x.Number <= 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") + return newNumberFormulaArg(math.Log(math.Gamma(x.Number))) } // GAMMALNdotPRECISE function returns the natural logarithm of the Gamma @@ -11709,11 +11659,7 @@ func (fn *formulaFuncs) AND(argsList *list.List) formulaArg { if argsList.Len() > 30 { return newErrorFormulaArg(formulaErrorVALUE, "AND accepts at most 30 arguments") } - var ( - and = true - val float64 - err error - ) + and := true for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { @@ -11726,10 +11672,9 @@ func (fn *formulaFuncs) AND(argsList *list.List) formulaArg { if token.String == "FALSE" { return newStringFormulaArg(token.String) } - if val, err = strconv.ParseFloat(token.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) - } - and = and && (val != 0) + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + case ArgNumber: + and = and && token.Number != 0 case ArgMatrix: // TODO return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) @@ -11845,11 +11790,7 @@ func (fn *formulaFuncs) OR(argsList *list.List) formulaArg { if argsList.Len() > 30 { return newErrorFormulaArg(formulaErrorVALUE, "OR accepts at most 30 arguments") } - var ( - or bool - val float64 - err error - ) + var or bool for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { @@ -11863,10 +11804,9 @@ func (fn *formulaFuncs) OR(argsList *list.List) formulaArg { or = true continue } - if val, err = strconv.ParseFloat(token.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) - } - or = val != 0 + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + case ArgNumber: + or = token.Number != 0 case ArgMatrix: // TODO return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) @@ -11931,20 +11871,6 @@ func calcXor(argsList *list.List) formulaArg { switch token.Type { case ArgError: return token - case ArgString: - if b := token.ToBool(); b.Type == ArgNumber { - ok = true - if b.Number == 1 { - count++ - } - continue - } - if num := token.ToNumber(); num.Type == ArgNumber { - ok = true - if num.Number != 0 { - count++ - } - } case ArgNumber: ok = true if token.Number != 0 { @@ -12907,7 +12833,7 @@ func (fn *formulaFuncs) CLEAN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "CLEAN requires 1 argument") } b := bytes.Buffer{} - for _, c := range argsList.Front().Value.(formulaArg).String { + for _, c := range argsList.Front().Value.(formulaArg).Value() { if c > 31 { b.WriteRune(c) } @@ -13477,7 +13403,7 @@ func (fn *formulaFuncs) TRIM(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "TRIM requires 1 argument") } - return newStringFormulaArg(strings.TrimSpace(argsList.Front().Value.(formulaArg).String)) + return newStringFormulaArg(strings.TrimSpace(argsList.Front().Value.(formulaArg).Value())) } // UNICHAR returns the Unicode character that is referenced by the given @@ -13584,27 +13510,30 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { if cond, err = strconv.ParseBool(token.String); err != nil { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if argsList.Len() == 1 { - return newBoolFormulaArg(cond) - } - if cond { - value := argsList.Front().Next().Value.(formulaArg) - switch value.Type { - case ArgNumber: - result = value.ToNumber() - default: - result = newStringFormulaArg(value.String) - } - return result + case ArgNumber: + cond = token.Number == 1 + } + + if argsList.Len() == 1 { + return newBoolFormulaArg(cond) + } + if cond { + value := argsList.Front().Next().Value.(formulaArg) + switch value.Type { + case ArgNumber: + result = value.ToNumber() + default: + result = newStringFormulaArg(value.String) } - if argsList.Len() == 3 { - value := argsList.Back().Value.(formulaArg) - switch value.Type { - case ArgNumber: - result = value.ToNumber() - default: - result = newStringFormulaArg(value.String) - } + return result + } + if argsList.Len() == 3 { + value := argsList.Back().Value.(formulaArg) + switch value.Type { + case ArgNumber: + result = value.ToNumber() + default: + result = newStringFormulaArg(value.String) } } return result @@ -13676,7 +13605,7 @@ func (fn *formulaFuncs) CHOOSE(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "CHOOSE requires 2 arguments") } - idx, err := strconv.Atoi(argsList.Front().Value.(formulaArg).String) + idx, err := strconv.Atoi(argsList.Front().Value.(formulaArg).Value()) if err != nil { return newErrorFormulaArg(formulaErrorVALUE, "CHOOSE requires first argument of type number") } @@ -14075,7 +14004,7 @@ func (fn *formulaFuncs) MATCH(argsList *list.List) formulaArg { default: return newErrorFormulaArg(formulaErrorNA, lookupArrayErr) } - return calcMatch(matchType, formulaCriteriaParser(argsList.Front().Value.(formulaArg).String), lookupArray) + return calcMatch(matchType, formulaCriteriaParser(argsList.Front().Value.(formulaArg).Value()), lookupArray) } // TRANSPOSE function 'transposes' an array of cells (i.e. the function copies @@ -14237,7 +14166,7 @@ func checkLookupArgs(argsList *list.List) (arrayForm bool, lookupValue, lookupVe errArg = newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires at most 3 arguments") return } - lookupValue = argsList.Front().Value.(formulaArg) + lookupValue = newStringFormulaArg(argsList.Front().Value.(formulaArg).Value()) lookupVector = argsList.Front().Next().Value.(formulaArg) if lookupVector.Type != ArgMatrix && lookupVector.Type != ArgList { errArg = newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires second argument of table array") diff --git a/calc_test.go b/calc_test.go index 6d8336293e..205f329bdf 100644 --- a/calc_test.go +++ b/calc_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/xuri/efp" ) func prepareCalcData(cellData [][]interface{}) *File { @@ -545,6 +544,7 @@ func TestCalcCellValue(t *testing.T) { // GCD "=GCD(0)": "0", "=GCD(1,0)": "1", + "=GCD(\"0\",1)": "1", "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", "=GCD(0,8,12)": "4", @@ -655,6 +655,7 @@ func TestCalcCellValue(t *testing.T) { "=PRODUCT(3,6)": "18", `=PRODUCT("",3,6)`: "18", `=PRODUCT(PRODUCT(1),3,6)`: "18", + "=PRODUCT(C1:C2)": "1", // QUOTIENT "=QUOTIENT(5,2)": "2", "=QUOTIENT(4.5,3.1)": "1", @@ -798,7 +799,7 @@ func TestCalcCellValue(t *testing.T) { "=SUMSQ(A1,B1,A2,B2,6)": "82", `=SUMSQ("",A1,B1,A2,B2,6)`: "82", `=SUMSQ(1,SUMSQ(1))`: "2", - "=SUMSQ(MUNIT(3))": "0", + "=SUMSQ(MUNIT(3))": "3", // SUMX2MY2 "=SUMX2MY2(A1:A4,B1:B4)": "-36", // SUMX2PY2 @@ -927,8 +928,8 @@ func TestCalcCellValue(t *testing.T) { // CORREL "=CORREL(A1:A5,B1:B5)": "1", // COUNT - "=COUNT()": "0", - "=COUNT(E1:F2,\"text\",1,INT(2))": "3", + "=COUNT()": "0", + "=COUNT(E1:F2,\"text\",1,INT(2),\"0\")": "4", // COUNTA "=COUNTA()": "0", "=COUNTA(A1:A5,B2:B5,\"text\",1,INT(2))": "8", @@ -959,19 +960,22 @@ func TestCalcCellValue(t *testing.T) { "=DEVSQ(1,3,5,2,9,7)": "47.5", "=DEVSQ(A1:D2)": "10", // FISHER - "=FISHER(-0.9)": "-1.47221948958322", - "=FISHER(-0.25)": "-0.255412811882995", - "=FISHER(0.8)": "1.09861228866811", - "=FISHER(INT(0))": "0", + "=FISHER(-0.9)": "-1.47221948958322", + "=FISHER(-0.25)": "-0.255412811882995", + "=FISHER(0.8)": "1.09861228866811", + "=FISHER(\"0.8\")": "1.09861228866811", + "=FISHER(INT(0))": "0", // FISHERINV "=FISHERINV(-0.2)": "-0.197375320224904", "=FISHERINV(INT(0))": "0", + "=FISHERINV(\"0\")": "0", "=FISHERINV(2.8)": "0.992631520201128", // GAMMA - "=GAMMA(0.1)": "9.51350769866873", - "=GAMMA(INT(1))": "1", - "=GAMMA(1.5)": "0.886226925452758", - "=GAMMA(5.5)": "52.3427777845535", + "=GAMMA(0.1)": "9.51350769866873", + "=GAMMA(INT(1))": "1", + "=GAMMA(1.5)": "0.886226925452758", + "=GAMMA(5.5)": "52.3427777845535", + "=GAMMA(\"5.5\")": "52.3427777845535", // GAMMA.DIST "=GAMMA.DIST(6,3,2,FALSE)": "0.112020903827694", "=GAMMA.DIST(6,3,2,TRUE)": "0.576809918873156", @@ -1097,12 +1101,13 @@ func TestCalcCellValue(t *testing.T) { "=LARGE(A1,1)": "1", "=LARGE(A1:F2,1)": "36693", // MAX - "=MAX(1)": "1", - "=MAX(TRUE())": "1", - "=MAX(0.5,TRUE())": "1", - "=MAX(FALSE())": "0", - "=MAX(MUNIT(2))": "1", - "=MAX(INT(1))": "1", + "=MAX(1)": "1", + "=MAX(TRUE())": "1", + "=MAX(0.5,TRUE())": "1", + "=MAX(FALSE())": "0", + "=MAX(MUNIT(2))": "1", + "=MAX(INT(1))": "1", + "=MAX(\"0\",\"2\")": "2", // MAXA "=MAXA(1)": "1", "=MAXA(TRUE())": "1", @@ -1117,6 +1122,7 @@ func TestCalcCellValue(t *testing.T) { "=MEDIAN(A1:A5,12)": "2", "=MEDIAN(A1:A5)": "1.5", "=MEDIAN(A1:A5,MEDIAN(A1:A5,12))": "2", + "=MEDIAN(\"0\",\"2\")": "1", // MIN "=MIN(1)": "1", "=MIN(TRUE())": "1", @@ -1124,6 +1130,7 @@ func TestCalcCellValue(t *testing.T) { "=MIN(FALSE())": "0", "=MIN(MUNIT(2))": "0", "=MIN(INT(1))": "1", + "=MIN(2,\"1\")": "1", // MINA "=MINA(1)": "1", "=MINA(TRUE())": "1", @@ -1345,14 +1352,15 @@ func TestCalcCellValue(t *testing.T) { "=T(N(10))": "", // Logical Functions // AND - "=AND(0)": "FALSE", - "=AND(1)": "TRUE", - "=AND(1,0)": "FALSE", - "=AND(0,1)": "FALSE", - "=AND(1=1)": "TRUE", - "=AND(1<2)": "TRUE", - "=AND(1>2,2<3,2>0,3>1)": "FALSE", - "=AND(1=1),1=1": "TRUE", + "=AND(0)": "FALSE", + "=AND(1)": "TRUE", + "=AND(1,0)": "FALSE", + "=AND(0,1)": "FALSE", + "=AND(1=1)": "TRUE", + "=AND(1<2)": "TRUE", + "=AND(1>2,2<3,2>0,3>1)": "FALSE", + "=AND(1=1),1=1": "TRUE", + "=AND(\"TRUE\",\"FALSE\")": "FALSE", // FALSE "=FALSE()": "FALSE", // IFERROR @@ -1372,10 +1380,11 @@ func TestCalcCellValue(t *testing.T) { "=NOT(\"true\")": "FALSE", "=NOT(ISBLANK(B1))": "TRUE", // OR - "=OR(1)": "TRUE", - "=OR(0)": "FALSE", - "=OR(1=2,2=2)": "TRUE", - "=OR(1=2,2=3)": "FALSE", + "=OR(1)": "TRUE", + "=OR(0)": "FALSE", + "=OR(1=2,2=2)": "TRUE", + "=OR(1=2,2=3)": "FALSE", + "=OR(\"TRUE\",\"FALSE\")": "TRUE", // SWITCH "=SWITCH(1,1,\"A\",2,\"B\",3,\"C\",\"N\")": "A", "=SWITCH(3,1,\"A\",2,\"B\",3,\"C\",\"N\")": "C", @@ -1897,6 +1906,7 @@ func TestCalcCellValue(t *testing.T) { // PRICEDISC "=PRICEDISC(\"04/01/2017\",\"03/31/2021\",2.5%,100)": "90", "=PRICEDISC(\"04/01/2017\",\"03/31/2021\",2.5%,100,3)": "90", + "=PRICEDISC(\"42826\",\"03/31/2021\",2.5%,100,3)": "90", // PRICEMAT "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%)": "107.170454545455", "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%,0)": "107.170454545455", @@ -2335,7 +2345,7 @@ func TestCalcCellValue(t *testing.T) { // _xlfn.DECIMAL "=_xlfn.DECIMAL()": "DECIMAL requires 2 numeric arguments", `=_xlfn.DECIMAL("X", 2)`: "strconv.ParseInt: parsing \"X\": invalid syntax", - `=_xlfn.DECIMAL(2000, "X")`: "strconv.Atoi: parsing \"X\": invalid syntax", + `=_xlfn.DECIMAL(2000, "X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // DEGREES "=DEGREES()": "DEGREES requires 1 numeric argument", `=DEGREES("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -2461,10 +2471,11 @@ func TestCalcCellValue(t *testing.T) { "=RANDBETWEEN()": "RANDBETWEEN requires 2 numeric arguments", "=RANDBETWEEN(2,1)": "#NUM!", // ROMAN - "=ROMAN()": "ROMAN requires at least 1 argument", - "=ROMAN(1,2,3)": "ROMAN allows at most 2 arguments", - `=ROMAN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=ROMAN("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ROMAN()": "ROMAN requires at least 1 argument", + "=ROMAN(1,2,3)": "ROMAN allows at most 2 arguments", + "=ROMAN(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=ROMAN(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=ROMAN(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", // ROUND "=ROUND()": "ROUND requires 2 numeric arguments", `=ROUND("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -2776,6 +2787,7 @@ func TestCalcCellValue(t *testing.T) { "=GAMMA()": "GAMMA requires 1 numeric argument", "=GAMMA(F1)": "GAMMA requires 1 numeric argument", "=GAMMA(0)": "#N/A", + "=GAMMA(\"0\")": "#N/A", "=GAMMA(INT(0))": "#N/A", // GAMMA.DIST "=GAMMA.DIST()": "GAMMA.DIST requires 4 arguments", @@ -3289,9 +3301,10 @@ func TestCalcCellValue(t *testing.T) { "=T(NA())": "#N/A", // Logical Functions // AND - `=AND("text")`: "strconv.ParseFloat: parsing \"text\": invalid syntax", - `=AND(A1:B1)`: "#VALUE!", - "=AND()": "AND requires at least 1 argument", + "=AND(\"text\")": "#VALUE!", + "=AND(A1:B1)": "#VALUE!", + "=AND(\"1\",\"TRUE\",\"FALSE\")": "#VALUE!", + "=AND()": "AND requires at least 1 argument", "=AND(1" + strings.Repeat(",1", 30) + ")": "AND accepts at most 30 arguments", // FALSE "=FALSE(A1)": "FALSE takes no arguments", @@ -3307,8 +3320,9 @@ func TestCalcCellValue(t *testing.T) { "=NOT(NOT())": "NOT requires 1 argument", "=NOT(\"\")": "NOT expects 1 boolean or numeric argument", // OR - `=OR("text")`: "strconv.ParseFloat: parsing \"text\": invalid syntax", - `=OR(A1:B1)`: "#VALUE!", + "=OR(\"text\")": "#VALUE!", + "=OR(A1:B1)": "#VALUE!", + "=OR(\"1\",\"TRUE\",\"FALSE\")": "#VALUE!", "=OR()": "OR requires at least 1 argument", "=OR(1" + strings.Repeat(",1", 30) + ")": "OR accepts at most 30 arguments", // SWITCH @@ -3318,6 +3332,7 @@ func TestCalcCellValue(t *testing.T) { "=TRUE(A1)": "TRUE takes no arguments", // XOR "=XOR()": "XOR requires at least 1 argument", + "=XOR(\"1\")": "#VALUE!", "=XOR(\"text\")": "#VALUE!", "=XOR(XOR(\"text\"))": "#VALUE!", // Date and Time Functions @@ -3595,7 +3610,7 @@ func TestCalcCellValue(t *testing.T) { "=HLOOKUP(D2,D1,1,FALSE)": "HLOOKUP requires second argument of table array", "=HLOOKUP(D2,D:D,FALSE,FALSE)": "HLOOKUP requires numeric row argument", "=HLOOKUP(D2,D:D,1,FALSE,FALSE)": "HLOOKUP requires at most 4 arguments", - "=HLOOKUP(D2,D:D,1,2)": "strconv.ParseBool: parsing \"2\": invalid syntax", + "=HLOOKUP(D2,D:D,1,2)": "HLOOKUP no result found", "=HLOOKUP(D2,D10:D10,1,FALSE)": "HLOOKUP no result found", "=HLOOKUP(D2,D2:D3,4,FALSE)": "HLOOKUP has invalid row index", "=HLOOKUP(D2,C:C,1,FALSE)": "HLOOKUP no result found", @@ -3616,7 +3631,7 @@ func TestCalcCellValue(t *testing.T) { "=VLOOKUP(D2,D1,1,FALSE)": "VLOOKUP requires second argument of table array", "=VLOOKUP(D2,D:D,FALSE,FALSE)": "VLOOKUP requires numeric col argument", "=VLOOKUP(D2,D:D,1,FALSE,FALSE)": "VLOOKUP requires at most 4 arguments", - "=VLOOKUP(D2,D:D,1,2)": "strconv.ParseBool: parsing \"2\": invalid syntax", + "=VLOOKUP(A1:A2,A1:A1,1)": "VLOOKUP no result found", "=VLOOKUP(D2,D10:D10,1,FALSE)": "VLOOKUP no result found", "=VLOOKUP(D2,D:D,2,FALSE)": "VLOOKUP has invalid column index", "=VLOOKUP(D2,C:C,1,FALSE)": "VLOOKUP no result found", @@ -4210,18 +4225,6 @@ func TestCalcCellValue(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestCalcCellValue.xlsx"))) } -func TestCalculate(t *testing.T) { - err := `strconv.ParseFloat: parsing "string": invalid syntax` - opd := NewStack() - opd.Push(efp.Token{TValue: "string"}) - opt := efp.Token{TValue: "-", TType: efp.TokenTypeOperatorPrefix} - assert.EqualError(t, calculate(opd, opt), err) - opd.Push(efp.Token{TValue: "string"}) - opd.Push(efp.Token{TValue: "string"}) - opt = efp.Token{TValue: "-", TType: efp.TokenTypeOperatorInfix} - assert.EqualError(t, calculate(opd, opt), err) -} - func TestCalcWithDefinedName(t *testing.T) { cellData := [][]interface{}{ {"A1_as_string", "B1_as_string", 123, nil}, @@ -4812,12 +4815,13 @@ func TestCalcSUMIFSAndAVERAGEIFS(t *testing.T) { assert.Equal(t, expected, result, formula) } calcError := map[string]string{ - "=AVERAGEIFS()": "AVERAGEIFS requires at least 3 arguments", - "=AVERAGEIFS(H1,\"\")": "AVERAGEIFS requires at least 3 arguments", - "=AVERAGEIFS(H1,\"\",TRUE,1)": "#N/A", - "=AVERAGEIFS(H1,\"\",TRUE)": "AVERAGEIF divide by zero", - "=SUMIFS()": "SUMIFS requires at least 3 arguments", - "=SUMIFS(D2:D13,A2:A13,1,B2:B13)": "#N/A", + "=AVERAGEIFS()": "AVERAGEIFS requires at least 3 arguments", + "=AVERAGEIFS(H1,\"\")": "AVERAGEIFS requires at least 3 arguments", + "=AVERAGEIFS(H1,\"\",TRUE,1)": "#N/A", + "=AVERAGEIFS(H1,\"\",TRUE)": "AVERAGEIF divide by zero", + "=SUMIFS()": "SUMIFS requires at least 3 arguments", + "=SUMIFS(D2:D13,A2:A13,1,B2:B13)": "#N/A", + "=SUMIFS(D20:D23,A2:A13,\">2\",C2:C13,\"Jeff\")": "#VALUE!", } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula)) @@ -4906,6 +4910,7 @@ func TestCalcXLOOKUP(t *testing.T) { "=XLOOKUP()": "XLOOKUP requires at least 3 arguments", "=XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,2,1)": "XLOOKUP allows at most 6 arguments", "=XLOOKUP($C3,$C5,$C6,NA(),0,2)": "#N/A", + "=XLOOKUP(\"?\",B2:B9,C2:C9,NA(),2)": "#N/A", "=XLOOKUP($C3,$C4:$D5,$C6:$C17,NA(),0,2)": "#VALUE!", "=XLOOKUP($C3,$C5:$C5,$C6:$G17,NA(),0,-2)": "#VALUE!", "=XLOOKUP($C3,$C5:$G5,$C6:$F7,NA(),0,2)": "#VALUE!", From df91b34a3f816a865ba6f9ea7707c668552df9d6 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 28 Apr 2022 15:33:25 +0800 Subject: [PATCH 036/213] This closes #1211, improve the compatibility with invalid internal styles count --- styles.go | 2 +- styles_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/styles.go b/styles.go index c04ca3b3b3..11f6f75019 100644 --- a/styles.go +++ b/styles.go @@ -2443,7 +2443,7 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a if borderID != 0 { xf.ApplyBorder = boolPtr(true) } - style.CellXfs.Count++ + style.CellXfs.Count = len(style.CellXfs.Xf) + 1 xf.Alignment = alignment if alignment != nil { xf.ApplyAlignment = boolPtr(applyAlignment) diff --git a/styles_test.go b/styles_test.go index a71041dd1e..156b4e33b1 100644 --- a/styles_test.go +++ b/styles_test.go @@ -271,14 +271,14 @@ func TestNewStyle(t *testing.T) { f.Styles.CellXfs.Xf = nil style4, err := f.NewStyle(&Style{NumFmt: 160, Lang: "unknown"}) assert.NoError(t, err) - assert.Equal(t, 1, style4) + assert.Equal(t, 0, style4) f = NewFile() f.Styles.NumFmts = nil f.Styles.CellXfs.Xf = nil style5, err := f.NewStyle(&Style{NumFmt: 160, Lang: "zh-cn"}) assert.NoError(t, err) - assert.Equal(t, 1, style5) + assert.Equal(t, 0, style5) } func TestGetDefaultFont(t *testing.T) { From 0f93bd23c97ac0f04fe8012bd4a262c851e44a82 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 29 Apr 2022 13:53:09 +0800 Subject: [PATCH 037/213] This closes #1213, fix get incorrect rich text value caused by missing cell type checking --- cell.go | 6 +++--- cell_test.go | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/cell.go b/cell.go index b2818e7fbd..3c44af45ec 100644 --- a/cell.go +++ b/cell.go @@ -764,7 +764,7 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro return } siIdx, err := strconv.Atoi(cellData.V) - if nil != err { + if err != nil || cellData.T != "s" { return } sst := f.sharedStringsReader() @@ -776,7 +776,7 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro run := RichTextRun{ Text: v.T.Val, } - if nil != v.RPr { + if v.RPr != nil { font := Font{Underline: "none"} font.Bold = v.RPr.B != nil font.Italic = v.RPr.I != nil @@ -793,7 +793,7 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro font.Size = *v.RPr.Sz.Val } font.Strike = v.RPr.Strike != nil - if nil != v.RPr.Color { + if v.RPr.Color != nil { font.Color = strings.TrimPrefix(v.RPr.Color.RGB, "FF") } run.Font = &font diff --git a/cell_test.go b/cell_test.go index 73b3018b3f..77179cc237 100644 --- a/cell_test.go +++ b/cell_test.go @@ -502,8 +502,13 @@ func TestGetCellRichText(t *testing.T) { }, } assert.NoError(t, f.SetCellRichText("Sheet1", "A1", runsSource)) + assert.NoError(t, f.SetCellValue("Sheet1", "A2", false)) - runs, err := f.GetCellRichText("Sheet1", "A1") + runs, err := f.GetCellRichText("Sheet1", "A2") + assert.NoError(t, err) + assert.Equal(t, []RichTextRun(nil), runs) + + runs, err = f.GetCellRichText("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, runsSource[0].Text, runs[0].Text) From 856ee57c4019b4478da0f6cb3010ae636914a6be Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 30 Apr 2022 09:54:11 +0800 Subject: [PATCH 038/213] This closes #1212, init support for 1900 or 1904 date system --- cell.go | 64 ++++++++++++++++++++++++++++-------------------- chart.go | 4 +-- date.go | 4 +-- numfmt.go | 8 +++--- numfmt_test.go | 2 +- picture.go | 4 +-- shape.go | 4 +-- styles.go | 16 ++++++------ workbook.go | 25 +++++++++++++++++-- workbook_test.go | 12 +++++++++ 10 files changed, 94 insertions(+), 49 deletions(-) diff --git a/cell.go b/cell.go index 3c44af45ec..1d6ed847f6 100644 --- a/cell.go +++ b/cell.go @@ -109,8 +109,12 @@ func (f *File) GetCellType(sheet, axis string) (CellType, error) { // bool // nil // -// Note that default date format is m/d/yy h:mm of time.Time type value. You can -// set numbers format by SetCellStyle() method. +// Note that default date format is m/d/yy h:mm of time.Time type value. You +// can set numbers format by SetCellStyle() method. If you need to set the +// specialized date in Excel like January 0, 1900 or February 29, 1900, these +// times can not representation in Go language time.Time data type. Please set +// the cell value as number 0 or 60, then create and bind the date-time number +// format style for the cell. func (f *File) SetCellValue(sheet, axis string, value interface{}) error { var err error switch v := value.(type) { @@ -240,7 +244,7 @@ func setCellTime(value time.Time) (t string, b string, isNum bool, err error) { // setCellDuration prepares cell type and value by given Go time.Duration type // time duration. func setCellDuration(value time.Duration) (t string, v string) { - v = strconv.FormatFloat(value.Seconds()/86400.0, 'f', -1, 32) + v = strconv.FormatFloat(value.Seconds()/86400, 'f', -1, 32) return } @@ -752,26 +756,8 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...Hype return nil } -// GetCellRichText provides a function to get rich text of cell by given -// worksheet. -func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err error) { - ws, err := f.workSheetReader(sheet) - if err != nil { - return - } - cellData, _, _, err := f.prepareCell(ws, cell) - if err != nil { - return - } - siIdx, err := strconv.Atoi(cellData.V) - if err != nil || cellData.T != "s" { - return - } - sst := f.sharedStringsReader() - if len(sst.SI) <= siIdx || siIdx < 0 { - return - } - si := sst.SI[siIdx] +// getCellRichText returns rich text of cell by given string item. +func getCellRichText(si *xlsxSI) (runs []RichTextRun) { for _, v := range si.R { run := RichTextRun{ Text: v.T.Val, @@ -803,6 +789,29 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro return } +// GetCellRichText provides a function to get rich text of cell by given +// worksheet. +func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err error) { + ws, err := f.workSheetReader(sheet) + if err != nil { + return + } + cellData, _, _, err := f.prepareCell(ws, cell) + if err != nil { + return + } + siIdx, err := strconv.Atoi(cellData.V) + if err != nil || cellData.T != "s" { + return + } + sst := f.sharedStringsReader() + if len(sst.SI) <= siIdx || siIdx < 0 { + return + } + runs = getCellRichText(&sst.SI[siIdx]) + return +} + // newRpr create run properties for the rich text by given font format. func newRpr(fnt *Font) *xlsxRPr { rpr := xlsxRPr{} @@ -1099,17 +1108,20 @@ func (f *File) formattedValue(s int, v string, raw bool) string { if styleSheet.CellXfs.Xf[s].NumFmtID != nil { numFmtID = *styleSheet.CellXfs.Xf[s].NumFmtID } - + date1904, wb := false, f.workbookReader() + if wb != nil && wb.WorkbookPr != nil { + date1904 = wb.WorkbookPr.Date1904 + } ok := builtInNumFmtFunc[numFmtID] if ok != nil { - return ok(v, builtInNumFmt[numFmtID]) + return ok(v, builtInNumFmt[numFmtID], date1904) } if styleSheet == nil || styleSheet.NumFmts == nil { return v } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { - return format(v, xlsxFmt.FormatCode) + return format(v, xlsxFmt.FormatCode, date1904) } } return v diff --git a/chart.go b/chart.go index f740a2b219..7b7162ba96 100644 --- a/chart.go +++ b/chart.go @@ -479,8 +479,8 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { }, Format: formatPicture{ FPrintsWithSheet: true, - XScale: 1.0, - YScale: 1.0, + XScale: 1, + YScale: 1, }, Legend: formatChartLegend{ Position: "bottom", diff --git a/date.go b/date.go index 83d23ccf78..1574af7844 100644 --- a/date.go +++ b/date.go @@ -36,7 +36,7 @@ func timeToExcelTime(t time.Time) (float64, error) { // TODO in future this should probably also handle date1904 and like TimeFromExcelTime if t.Before(excelMinTime1900) { - return 0.0, nil + return 0, nil } tt := t @@ -58,7 +58,7 @@ func timeToExcelTime(t time.Time) (float64, error) { // program that had the majority market share at the time; Lotus 1-2-3. // https://www.myonlinetraininghub.com/excel-date-and-time if t.After(excelBuggyPeriodStart) { - result += 1.0 + result++ } return result, nil } diff --git a/numfmt.go b/numfmt.go index 6cb7fc7493..5503027a92 100644 --- a/numfmt.go +++ b/numfmt.go @@ -34,7 +34,7 @@ type numberFormat struct { section []nfp.Section t time.Time sectionIdx int - isNumeric, hours, seconds bool + date1904, isNumeric, hours, seconds bool number float64 ap, afterPoint, beforePoint, localCode, result, value, valueSectionType string } @@ -287,9 +287,9 @@ func (nf *numberFormat) prepareNumberic(value string) { // format provides a function to return a string parse by number format // expression. If the given number format is not supported, this will return // the original cell value. -func format(value, numFmt string) string { +func format(value, numFmt string, date1904 bool) string { p := nfp.NumberFormatParser() - nf := numberFormat{section: p.Parse(numFmt), value: value} + nf := numberFormat{section: p.Parse(numFmt), value: value, date1904: date1904} nf.number, nf.valueSectionType = nf.getValueSectionType(value) nf.prepareNumberic(value) for i, section := range nf.section { @@ -315,7 +315,7 @@ func format(value, numFmt string) string { // positiveHandler will be handling positive selection for a number format // expression. func (nf *numberFormat) positiveHandler() (result string) { - nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, false), false, false + nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false for i, token := range nf.section[nf.sectionIdx].Items { if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { result = nf.value diff --git a/numfmt_test.go b/numfmt_test.go index 7dc3f770b2..5cdf56bc4a 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -1005,7 +1005,7 @@ func TestNumFmt(t *testing.T) { {"-8.0450685976001E-21", "0_);[Red]\\(0\\)", "(0)"}, {"-8.04506", "0_);[Red]\\(0\\)", "(8)"}, } { - result := format(item[0], item[1]) + result := format(item[0], item[1], false) assert.Equal(t, item[2], result, item) } } diff --git a/picture.go b/picture.go index 919262c99d..5e8f6b874d 100644 --- a/picture.go +++ b/picture.go @@ -31,8 +31,8 @@ import ( func parseFormatPictureSet(formatSet string) (*formatPicture, error) { format := formatPicture{ FPrintsWithSheet: true, - XScale: 1.0, - YScale: 1.0, + XScale: 1, + YScale: 1, } err := json.Unmarshal(parseFormatSet(formatSet), &format) return &format, err diff --git a/shape.go b/shape.go index db76867355..6d86f3800d 100644 --- a/shape.go +++ b/shape.go @@ -25,8 +25,8 @@ func parseFormatShapeSet(formatSet string) (*formatShape, error) { Height: 160, Format: formatPicture{ FPrintsWithSheet: true, - XScale: 1.0, - YScale: 1.0, + XScale: 1, + YScale: 1, }, Line: formatLine{Width: 1}, } diff --git a/styles.go b/styles.go index 11f6f75019..6ef7dcbe20 100644 --- a/styles.go +++ b/styles.go @@ -754,7 +754,7 @@ var currencyNumFmt = map[int]string{ // builtInNumFmtFunc defined the format conversion functions map. Partial format // code doesn't support currently and will return original string. -var builtInNumFmtFunc = map[int]func(v string, format string) string{ +var builtInNumFmtFunc = map[int]func(v, format string, date1904 bool) string{ 0: format, 1: formatToInt, 2: formatToFloat, @@ -847,7 +847,7 @@ var criteriaType = map[string]string{ // formatToInt provides a function to convert original string to integer // format as string type by given built-in number formats code and cell // string. -func formatToInt(v string, format string) string { +func formatToInt(v, format string, date1904 bool) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -858,7 +858,7 @@ func formatToInt(v string, format string) string { // formatToFloat provides a function to convert original string to float // format as string type by given built-in number formats code and cell // string. -func formatToFloat(v string, format string) string { +func formatToFloat(v, format string, date1904 bool) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -868,7 +868,7 @@ func formatToFloat(v string, format string) string { // formatToA provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. -func formatToA(v string, format string) string { +func formatToA(v, format string, date1904 bool) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -883,7 +883,7 @@ func formatToA(v string, format string) string { // formatToB provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. -func formatToB(v string, format string) string { +func formatToB(v, format string, date1904 bool) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -896,7 +896,7 @@ func formatToB(v string, format string) string { // formatToC provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. -func formatToC(v string, format string) string { +func formatToC(v, format string, date1904 bool) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -907,7 +907,7 @@ func formatToC(v string, format string) string { // formatToD provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. -func formatToD(v string, format string) string { +func formatToD(v, format string, date1904 bool) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -918,7 +918,7 @@ func formatToD(v string, format string) string { // formatToE provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. -func formatToE(v string, format string) string { +func formatToE(v, format string, date1904 bool) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v diff --git a/workbook.go b/workbook.go index c65397b6fc..417524b1da 100644 --- a/workbook.go +++ b/workbook.go @@ -33,6 +33,10 @@ type WorkbookPrOptionPtr interface { } type ( + // Date1904 is an option used for WorkbookPrOption, that indicates whether + // to use a 1900 or 1904 date system when converting serial date-times in + // the workbook to dates + Date1904 bool // FilterPrivacy is an option used for WorkbookPrOption FilterPrivacy bool ) @@ -116,6 +120,7 @@ func (f *File) workBookWriter() { // SetWorkbookPrOptions provides a function to sets workbook properties. // // Available options: +// Date1904(bool) // FilterPrivacy(bool) // CodeName(string) func (f *File) SetWorkbookPrOptions(opts ...WorkbookPrOption) error { @@ -131,6 +136,11 @@ func (f *File) SetWorkbookPrOptions(opts ...WorkbookPrOption) error { return nil } +// setWorkbookPrOption implements the WorkbookPrOption interface. +func (o Date1904) setWorkbookPrOption(pr *xlsxWorkbookPr) { + pr.Date1904 = bool(o) +} + // setWorkbookPrOption implements the WorkbookPrOption interface. func (o FilterPrivacy) setWorkbookPrOption(pr *xlsxWorkbookPr) { pr.FilterPrivacy = bool(o) @@ -144,6 +154,7 @@ func (o CodeName) setWorkbookPrOption(pr *xlsxWorkbookPr) { // GetWorkbookPrOptions provides a function to gets workbook properties. // // Available options: +// Date1904(bool) // FilterPrivacy(bool) // CodeName(string) func (f *File) GetWorkbookPrOptions(opts ...WorkbookPrOptionPtr) error { @@ -156,7 +167,17 @@ func (f *File) GetWorkbookPrOptions(opts ...WorkbookPrOptionPtr) error { } // getWorkbookPrOption implements the WorkbookPrOption interface and get the -// filter privacy of thw workbook. +// date1904 of the workbook. +func (o *Date1904) getWorkbookPrOption(pr *xlsxWorkbookPr) { + if pr == nil { + *o = false + return + } + *o = Date1904(pr.Date1904) +} + +// getWorkbookPrOption implements the WorkbookPrOption interface and get the +// filter privacy of the workbook. func (o *FilterPrivacy) getWorkbookPrOption(pr *xlsxWorkbookPr) { if pr == nil { *o = false @@ -166,7 +187,7 @@ func (o *FilterPrivacy) getWorkbookPrOption(pr *xlsxWorkbookPr) { } // getWorkbookPrOption implements the WorkbookPrOption interface and get the -// code name of thw workbook. +// code name of the workbook. func (o *CodeName) getWorkbookPrOption(pr *xlsxWorkbookPr) { if pr == nil { *o = "" diff --git a/workbook_test.go b/workbook_test.go index e31caf26ac..18b222c00f 100644 --- a/workbook_test.go +++ b/workbook_test.go @@ -10,6 +10,7 @@ import ( func ExampleFile_SetWorkbookPrOptions() { f := NewFile() if err := f.SetWorkbookPrOptions( + Date1904(false), FilterPrivacy(false), CodeName("code"), ); err != nil { @@ -21,9 +22,13 @@ func ExampleFile_SetWorkbookPrOptions() { func ExampleFile_GetWorkbookPrOptions() { f := NewFile() var ( + date1904 Date1904 filterPrivacy FilterPrivacy codeName CodeName ) + if err := f.GetWorkbookPrOptions(&date1904); err != nil { + fmt.Println(err) + } if err := f.GetWorkbookPrOptions(&filterPrivacy); err != nil { fmt.Println(err) } @@ -31,10 +36,12 @@ func ExampleFile_GetWorkbookPrOptions() { fmt.Println(err) } fmt.Println("Defaults:") + fmt.Printf("- date1904: %t\n", date1904) fmt.Printf("- filterPrivacy: %t\n", filterPrivacy) fmt.Printf("- codeName: %q\n", codeName) // Output: // Defaults: + // - date1904: false // - filterPrivacy: true // - codeName: "" } @@ -42,6 +49,11 @@ func ExampleFile_GetWorkbookPrOptions() { func TestWorkbookPr(t *testing.T) { f := NewFile() wb := f.workbookReader() + wb.WorkbookPr = nil + var date1904 Date1904 + assert.NoError(t, f.GetWorkbookPrOptions(&date1904)) + assert.Equal(t, false, bool(date1904)) + wb.WorkbookPr = nil var codeName CodeName assert.NoError(t, f.GetWorkbookPrOptions(&codeName)) From 773d4afa32a55349a7b178c4c76d182f9ed0221f Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 1 May 2022 12:28:36 +0800 Subject: [PATCH 039/213] This closes #1217, support update cell hyperlink Ref #1129, make `SetRowStyle` overwrite style of the cells --- cell.go | 24 +++++++++++++++++------- excelize.go | 22 ++++++++++++++++++++++ excelize_test.go | 16 +++++++++++----- rows.go | 5 +++++ rows_test.go | 15 +++++++++------ 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/cell.go b/cell.go index 1d6ed847f6..70832cef83 100644 --- a/cell.go +++ b/cell.go @@ -715,10 +715,17 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...Hype } var linkData xlsxHyperlink - + idx := -1 if ws.Hyperlinks == nil { ws.Hyperlinks = new(xlsxHyperlinks) } + for i, hyperlink := range ws.Hyperlinks.Hyperlink { + if hyperlink.Ref == axis { + idx = i + linkData = hyperlink + break + } + } if len(ws.Hyperlinks.Hyperlink) > TotalSheetHyperlinks { return ErrTotalSheetHyperlinks @@ -726,12 +733,12 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...Hype switch linkType { case "External": + sheetPath := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + rID := f.setRels(linkData.RID, sheetRels, SourceRelationshipHyperLink, link, linkType) linkData = xlsxHyperlink{ Ref: axis, } - sheetPath := f.sheetMap[trimSheetName(sheet)] - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" - rID := f.addRels(sheetRels, SourceRelationshipHyperLink, link, linkType) linkData.RID = "rId" + strconv.Itoa(rID) f.addSheetNameSpace(sheet, SourceRelationship) case "Location": @@ -751,9 +758,12 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...Hype linkData.Tooltip = *o.Tooltip } } - - ws.Hyperlinks.Hyperlink = append(ws.Hyperlinks.Hyperlink, linkData) - return nil + if idx == -1 { + ws.Hyperlinks.Hyperlink = append(ws.Hyperlinks.Hyperlink, linkData) + return err + } + ws.Hyperlinks.Hyperlink[idx] = linkData + return err } // getCellRichText returns rich text of cell by given string item. diff --git a/excelize.go b/excelize.go index 9fe3d8816d..d78d2b1ea5 100644 --- a/excelize.go +++ b/excelize.go @@ -327,6 +327,28 @@ func checkSheetR0(ws *xlsxWorksheet, sheetData *xlsxSheetData, r0 *xlsxRow) { ws.SheetData = *sheetData } +// setRels provides a function to set relationships by given relationship ID, +// XML path, relationship type, target and target mode. +func (f *File) setRels(rID, relPath, relType, target, targetMode string) int { + rels := f.relsReader(relPath) + if rels == nil || rID == "" { + return f.addRels(relPath, relType, target, targetMode) + } + rels.Lock() + defer rels.Unlock() + var ID int + for i, rel := range rels.Relationships { + if rel.ID == rID { + rels.Relationships[i].Type = relType + rels.Relationships[i].Target = target + rels.Relationships[i].TargetMode = targetMode + ID, _ = strconv.Atoi(strings.TrimPrefix(rID, "rId")) + break + } + } + return ID +} + // addRels provides a function to add relationships by given XML path, // relationship type, target and target mode. func (f *File) addRels(relPath, relType, target, targetMode string) int { diff --git a/excelize_test.go b/excelize_test.go index dc5dfccf80..389573e296 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -336,9 +336,7 @@ func TestAddDrawingVML(t *testing.T) { func TestSetCellHyperLink(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if err != nil { - t.Log(err) - } + assert.NoError(t, err) // Test set cell hyperlink in a work sheet already have hyperlinks. assert.NoError(t, f.SetCellHyperLink("Sheet1", "B19", "https://github.com/xuri/excelize", "External")) // Test add first hyperlink in a work sheet. @@ -346,8 +344,7 @@ func TestSetCellHyperLink(t *testing.T) { // Test add Location hyperlink in a work sheet. assert.NoError(t, f.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location")) // Test add Location hyperlink with display & tooltip in a work sheet. - display := "Display value" - tooltip := "Hover text" + display, tooltip := "Display value", "Hover text" assert.NoError(t, f.SetCellHyperLink("Sheet2", "D7", "Sheet1!D9", "Location", HyperlinkOpts{ Display: &display, Tooltip: &tooltip, @@ -376,6 +373,15 @@ func TestSetCellHyperLink(t *testing.T) { ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/xuri/excelize", "External") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + + // Test update cell hyperlink + f = NewFile() + assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "https://github.com", "External")) + assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "https://github.com/xuri/excelize", "External")) + link, target, err := f.GetCellHyperLink("Sheet1", "A1") + assert.Equal(t, link, true) + assert.Equal(t, "https://github.com/xuri/excelize", target) + assert.NoError(t, err) } func TestGetCellHyperLink(t *testing.T) { diff --git a/rows.go b/rows.go index e0918bc60e..bcb8960de6 100644 --- a/rows.go +++ b/rows.go @@ -841,6 +841,11 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error { for row := start - 1; row < end; row++ { ws.SheetData.Row[row].S = styleID ws.SheetData.Row[row].CustomFormat = true + for i := range ws.SheetData.Row[row].C { + if _, rowNum, err := CellNameToCoordinates(ws.SheetData.Row[row].C[i].R); err == nil && rowNum-1 == row { + ws.SheetData.Row[row].C[i].S = styleID + } + } } return nil } diff --git a/rows_test.go b/rows_test.go index 22b038aa8a..ae3083811a 100644 --- a/rows_test.go +++ b/rows_test.go @@ -915,16 +915,19 @@ func TestCheckRow(t *testing.T) { func TestSetRowStyle(t *testing.T) { f := NewFile() - styleID, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) + style1, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#63BE7B"],"pattern":1}}`) assert.NoError(t, err) - assert.EqualError(t, f.SetRowStyle("Sheet1", 10, -1, styleID), newInvalidRowNumberError(-1).Error()) - assert.EqualError(t, f.SetRowStyle("Sheet1", 1, TotalRows+1, styleID), ErrMaxRows.Error()) + style2, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) + assert.NoError(t, err) + assert.NoError(t, f.SetCellStyle("Sheet1", "B2", "B2", style1)) + assert.EqualError(t, f.SetRowStyle("Sheet1", 5, -1, style2), newInvalidRowNumberError(-1).Error()) + assert.EqualError(t, f.SetRowStyle("Sheet1", 1, TotalRows+1, style2), ErrMaxRows.Error()) assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, -1), newInvalidStyleID(-1).Error()) - assert.EqualError(t, f.SetRowStyle("SheetN", 1, 1, styleID), "sheet SheetN is not exist") - assert.NoError(t, f.SetRowStyle("Sheet1", 10, 1, styleID)) + assert.EqualError(t, f.SetRowStyle("SheetN", 1, 1, style2), "sheet SheetN is not exist") + assert.NoError(t, f.SetRowStyle("Sheet1", 5, 1, style2)) cellStyleID, err := f.GetCellStyle("Sheet1", "B2") assert.NoError(t, err) - assert.Equal(t, styleID, cellStyleID) + assert.Equal(t, style2, cellStyleID) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRowStyle.xlsx"))) } From eed431e0fc2f61b13e7745857a41cb47d9f7f810 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 2 May 2022 12:30:18 +0800 Subject: [PATCH 040/213] This closes #1219, fixes cell value reading issue, improves performance, and 1904 date system support - Fix incorrect cell data types casting results when number formatting - Support set cell value on 1904 date system enabled, ref #1212 - Improve performance for set sheet row and the merging cells, fix performance impact when resolving #1129 --- cell.go | 23 ++++++++++++++--------- cell_test.go | 2 +- date.go | 20 +++++++++----------- date_test.go | 33 +++++++++++++++++++++++---------- merge.go | 7 ++----- numfmt.go | 7 ++++--- rows_test.go | 5 +++++ stream.go | 6 +++++- 8 files changed, 63 insertions(+), 40 deletions(-) diff --git a/cell.go b/cell.go index 70832cef83..80c03efe05 100644 --- a/cell.go +++ b/cell.go @@ -211,9 +211,12 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { ws.Lock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) ws.Unlock() - + date1904, wb := false, f.workbookReader() + if wb != nil && wb.WorkbookPr != nil { + date1904 = wb.WorkbookPr.Date1904 + } var isNum bool - cellData.T, cellData.V, isNum, err = setCellTime(value) + cellData.T, cellData.V, isNum, err = setCellTime(value, date1904) if err != nil { return err } @@ -225,11 +228,11 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { // setCellTime prepares cell type and Excel time by given Go time.Time type // timestamp. -func setCellTime(value time.Time) (t string, b string, isNum bool, err error) { +func setCellTime(value time.Time, date1904 bool) (t string, b string, isNum bool, err error) { var excelTime float64 _, offset := value.In(value.Location()).Zone() value = value.Add(time.Duration(offset) * time.Second) - if excelTime, err = timeToExcelTime(value); err != nil { + if excelTime, err = timeToExcelTime(value, date1904); err != nil { return } isNum = excelTime > 0 @@ -1122,8 +1125,7 @@ func (f *File) formattedValue(s int, v string, raw bool) string { if wb != nil && wb.WorkbookPr != nil { date1904 = wb.WorkbookPr.Date1904 } - ok := builtInNumFmtFunc[numFmtID] - if ok != nil { + if ok := builtInNumFmtFunc[numFmtID]; ok != nil { return ok(v, builtInNumFmt[numFmtID], date1904) } if styleSheet == nil || styleSheet.NumFmts == nil { @@ -1140,15 +1142,18 @@ func (f *File) formattedValue(s int, v string, raw bool) string { // prepareCellStyle provides a function to prepare style index of cell in // worksheet by given column index and style index. func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int { - if ws.Cols != nil && style == 0 { + if style != 0 { + return style + } + if ws.Cols != nil { for _, c := range ws.Cols.Col { if c.Min <= col && col <= c.Max && c.Style != 0 { return c.Style } } } - for rowIdx := range ws.SheetData.Row { - if styleID := ws.SheetData.Row[rowIdx].S; style == 0 && styleID != 0 { + if row <= len(ws.SheetData.Row) { + if styleID := ws.SheetData.Row[row-1].S; styleID != 0 { return styleID } } diff --git a/cell_test.go b/cell_test.go index 77179cc237..8ed8e1f6c9 100644 --- a/cell_test.go +++ b/cell_test.go @@ -192,7 +192,7 @@ func TestSetCellTime(t *testing.T) { } { timezone, err := time.LoadLocation(location) assert.NoError(t, err) - _, b, isNum, err := setCellTime(date.In(timezone)) + _, b, isNum, err := setCellTime(date.In(timezone), false) assert.NoError(t, err) assert.Equal(t, true, isNum) assert.Equal(t, expected, b) diff --git a/date.go b/date.go index 1574af7844..3e81319dd7 100644 --- a/date.go +++ b/date.go @@ -32,21 +32,19 @@ var ( ) // timeToExcelTime provides a function to convert time to Excel time. -func timeToExcelTime(t time.Time) (float64, error) { - // TODO in future this should probably also handle date1904 and like TimeFromExcelTime - - if t.Before(excelMinTime1900) { +func timeToExcelTime(t time.Time, date1904 bool) (float64, error) { + date := excelMinTime1900 + if date1904 { + date = excel1904Epoc + } + if t.Before(date) { return 0, nil } - - tt := t - diff := t.Sub(excelMinTime1900) - result := float64(0) - + tt, diff, result := t, t.Sub(date), 0.0 for diff >= maxDuration { result += float64(maxDuration / dayNanoseconds) tt = tt.Add(-maxDuration) - diff = tt.Sub(excelMinTime1900) + diff = tt.Sub(date) } rem := diff % dayNanoseconds @@ -57,7 +55,7 @@ func timeToExcelTime(t time.Time) (float64, error) { // Microsoft intentionally included this bug in Excel so that it would remain compatible with the spreadsheet // program that had the majority market share at the time; Lotus 1-2-3. // https://www.myonlinetraininghub.com/excel-date-and-time - if t.After(excelBuggyPeriodStart) { + if !date1904 && t.After(excelBuggyPeriodStart) { result++ } return result, nil diff --git a/date_test.go b/date_test.go index cc21e58a3f..4091e378db 100644 --- a/date_test.go +++ b/date_test.go @@ -40,7 +40,7 @@ var excelTimeInputList = []dateTest{ func TestTimeToExcelTime(t *testing.T) { for i, test := range trueExpectedDateList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { - excelTime, err := timeToExcelTime(test.GoValue) + excelTime, err := timeToExcelTime(test.GoValue, false) assert.NoError(t, err) assert.Equalf(t, test.ExcelValue, excelTime, "Time: %s", test.GoValue.String()) @@ -55,7 +55,7 @@ func TestTimeToExcelTime_Timezone(t *testing.T) { } for i, test := range trueExpectedDateList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { - _, err := timeToExcelTime(test.GoValue.In(location)) + _, err := timeToExcelTime(test.GoValue.In(location), false) assert.NoError(t, err) }) } @@ -71,21 +71,34 @@ func TestTimeFromExcelTime(t *testing.T) { for min := 0; min < 60; min++ { for sec := 0; sec < 60; sec++ { date := time.Date(2021, time.December, 30, hour, min, sec, 0, time.UTC) - excelTime, err := timeToExcelTime(date) + // Test use 1900 date system + excel1900Time, err := timeToExcelTime(date, false) assert.NoError(t, err) - dateOut := timeFromExcelTime(excelTime, false) - assert.EqualValues(t, hour, dateOut.Hour()) - assert.EqualValues(t, min, dateOut.Minute()) - assert.EqualValues(t, sec, dateOut.Second()) + date1900Out := timeFromExcelTime(excel1900Time, false) + assert.EqualValues(t, hour, date1900Out.Hour()) + assert.EqualValues(t, min, date1900Out.Minute()) + assert.EqualValues(t, sec, date1900Out.Second()) + // Test use 1904 date system + excel1904Time, err := timeToExcelTime(date, true) + assert.NoError(t, err) + date1904Out := timeFromExcelTime(excel1904Time, true) + assert.EqualValues(t, hour, date1904Out.Hour()) + assert.EqualValues(t, min, date1904Out.Minute()) + assert.EqualValues(t, sec, date1904Out.Second()) } } } } func TestTimeFromExcelTime_1904(t *testing.T) { - _, _ = shiftJulianToNoon(1, -0.6) - timeFromExcelTime(61, true) - timeFromExcelTime(62, true) + julianDays, julianFraction := shiftJulianToNoon(1, -0.6) + assert.Equal(t, julianDays, 0.0) + assert.Equal(t, julianFraction, 0.9) + julianDays, julianFraction = shiftJulianToNoon(1, 0.1) + assert.Equal(t, julianDays, 1.0) + assert.Equal(t, julianFraction, 0.6) + assert.Equal(t, timeFromExcelTime(61, true), time.Date(1904, time.March, 2, 0, 0, 0, 0, time.UTC)) + assert.Equal(t, timeFromExcelTime(62, true), time.Date(1904, time.March, 3, 0, 0, 0, 0, time.UTC)) } func TestExcelDateToTime(t *testing.T) { diff --git a/merge.go b/merge.go index 376b68b29b..0f57826e37 100644 --- a/merge.go +++ b/merge.go @@ -11,9 +11,7 @@ package excelize -import ( - "strings" -) +import "strings" // Rect gets merged cell rectangle coordinates sequence. func (mc *xlsxMergeCell) Rect() ([]int, error) { @@ -70,8 +68,7 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error { ws.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref, rect: rect}}} } ws.MergeCells.Count = len(ws.MergeCells.Cells) - styleID, _ := f.GetCellStyle(sheet, hCell) - return f.SetCellStyle(sheet, hCell, vCell, styleID) + return err } // UnmergeCell provides a function to unmerge a given coordinate area. diff --git a/numfmt.go b/numfmt.go index 5503027a92..2052fd9b5d 100644 --- a/numfmt.go +++ b/numfmt.go @@ -939,10 +939,11 @@ func (nf *numberFormat) textHandler() (result string) { // getValueSectionType returns its applicable number format expression section // based on the given value. func (nf *numberFormat) getValueSectionType(value string) (float64, string) { - number, err := strconv.ParseFloat(value, 64) - if err != nil { - return number, nfp.TokenSectionText + isNum, _ := isNumeric(value) + if !isNum { + return 0, nfp.TokenSectionText } + number, _ := strconv.ParseFloat(value, 64) if number > 0 { return number, nfp.TokenSectionPositive } diff --git a/rows_test.go b/rows_test.go index ae3083811a..014b2d853f 100644 --- a/rows_test.go +++ b/rows_test.go @@ -928,6 +928,11 @@ func TestSetRowStyle(t *testing.T) { cellStyleID, err := f.GetCellStyle("Sheet1", "B2") assert.NoError(t, err) assert.Equal(t, style2, cellStyleID) + // Test cell inheritance rows style + assert.NoError(t, f.SetCellValue("Sheet1", "C1", nil)) + cellStyleID, err = f.GetCellStyle("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, style2, cellStyleID) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRowStyle.xlsx"))) } diff --git a/stream.go b/stream.go index c2eda68a40..e1a12bec00 100644 --- a/stream.go +++ b/stream.go @@ -440,7 +440,11 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) (err error) { c.T, c.V = setCellDuration(val) case time.Time: var isNum bool - c.T, c.V, isNum, err = setCellTime(val) + date1904, wb := false, sw.File.workbookReader() + if wb != nil && wb.WorkbookPr != nil { + date1904 = wb.WorkbookPr.Date1904 + } + c.T, c.V, isNum, err = setCellTime(val, date1904) if isNum && c.S == 0 { style, _ := sw.File.NewStyle(&Style{NumFmt: 22}) c.S = style From 0c3fd0223c784ddcc7d2442105b920587b970727 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 13 May 2022 01:03:40 +0800 Subject: [PATCH 041/213] This closes #1225, allowing insert EMF format images --- picture.go | 29 +++++++++++++---------------- picture_test.go | 13 +++++++++++-- sheet.go | 2 +- test/images/excel.emf | Bin 0 -> 12020 bytes xmlDrawing.go | 3 ++- 5 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 test/images/excel.emf diff --git a/picture.go b/picture.go index 5e8f6b874d..f8133ca86f 100644 --- a/picture.go +++ b/picture.go @@ -113,7 +113,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { if _, err = os.Stat(picture); os.IsNotExist(err) { return err } - ext, ok := supportImageTypes[path.Ext(picture)] + ext, ok := supportedImageTypes[path.Ext(picture)] if !ok { return ErrImgExt } @@ -154,7 +154,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, file []byte) error { var drawingHyperlinkRID int var hyperlinkType string - ext, ok := supportImageTypes[extension] + ext, ok := supportedImageTypes[extension] if !ok { return ErrImgExt } @@ -366,23 +366,20 @@ func (f *File) addMedia(file []byte, ext string) string { // setContentTypePartImageExtensions provides a function to set the content // type for relationship parts and the Main Document part. func (f *File) setContentTypePartImageExtensions() { - imageTypes := map[string]bool{"jpeg": false, "png": false, "gif": false, "tiff": false} + imageTypes := map[string]string{"jpeg": "image/", "png": "image/", "gif": "image/", "tiff": "image/", "emf": "image/x-"} content := f.contentTypesReader() content.Lock() defer content.Unlock() - for _, v := range content.Defaults { - _, ok := imageTypes[v.Extension] - if ok { - imageTypes[v.Extension] = true + for _, file := range content.Defaults { + if _, ok := imageTypes[file.Extension]; ok { + delete(imageTypes, file.Extension) } } - for k, v := range imageTypes { - if !v { - content.Defaults = append(content.Defaults, xlsxDefault{ - Extension: k, - ContentType: "image/" + k, - }) - } + for extension, prefix := range imageTypes { + content.Defaults = append(content.Defaults, xlsxDefault{ + Extension: extension, + ContentType: prefix + extension, + }) } } @@ -576,7 +573,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic != nil { if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed) - if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { + if _, ok = supportedImageTypes[filepath.Ext(drawRel.Target)]; ok { ret = filepath.Base(drawRel.Target) if buffer, _ := f.Pkg.Load(strings.Replace(drawRel.Target, "..", "xl", -1)); buffer != nil { buf = buffer.([]byte) @@ -605,7 +602,7 @@ func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsD if anchor.From.Col == col && anchor.From.Row == row { if drawRel = f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed); drawRel != nil { - if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { + if _, ok = supportedImageTypes[filepath.Ext(drawRel.Target)]; ok { ret = filepath.Base(drawRel.Target) if buffer, _ := f.Pkg.Load(strings.Replace(drawRel.Target, "..", "xl", -1)); buffer != nil { buf = buffer.([]byte) diff --git a/picture_test.go b/picture_test.go index fbbdf114b5..c5480352ac 100644 --- a/picture_test.go +++ b/picture_test.go @@ -2,9 +2,11 @@ package excelize import ( "fmt" + "image" _ "image/gif" _ "image/jpeg" _ "image/png" + "io" "io/ioutil" "os" "path/filepath" @@ -66,7 +68,7 @@ func TestAddPicture(t *testing.T) { assert.NoError(t, f.AddPicture("Sheet1", "Q22", filepath.Join("test", "images", "excel.tif"), "")) // Test write file to given path. - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx"))) assert.NoError(t, f.Close()) } @@ -89,7 +91,14 @@ func TestAddPictureErrors(t *testing.T) { // Test add picture to worksheet with invalid file data. err = f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)) - assert.EqualError(t, err, "image: unknown format") + assert.EqualError(t, err, image.ErrFormat.Error()) + + // Test add picture with custom image decoder and encoder. + decode := func(r io.Reader) (image.Image, error) { return nil, nil } + decodeConfig := func(r io.Reader) (image.Config, error) { return image.Config{Height: 100, Width: 90}, nil } + image.RegisterFormat("emf", "", decode, decodeConfig) + assert.NoError(t, f.AddPicture("Sheet1", "Q1", filepath.Join("test", "images", "excel.emf"), "")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx"))) assert.NoError(t, f.Close()) } diff --git a/sheet.go b/sheet.go index 3986cd86ad..4665fd9157 100644 --- a/sheet.go +++ b/sheet.go @@ -463,7 +463,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { if _, err = os.Stat(picture); os.IsNotExist(err) { return err } - ext, ok := supportImageTypes[path.Ext(picture)] + ext, ok := supportedImageTypes[path.Ext(picture)] if !ok { return ErrImgExt } diff --git a/test/images/excel.emf b/test/images/excel.emf new file mode 100644 index 0000000000000000000000000000000000000000..9daa6de8b2ffc18f05d94a66460a94a0fb119ae3 GIT binary patch literal 12020 zcmcgyTWl4_86K0gK+f?EUoa3HlYm-+Z-hAJ9vd*&#!!+VN`RIUkracu1W1dGTa%=P zw0UTi1X7f$eW(Z_4lhwvMarcXpsEj1Ri)ulNYtk^s`}7WL2aZ~-G1N9{yTHl>pkbJ zZC3wxW_EXW_WS?;o7p|uDNsrs2DrWKdERDI)xQm&d)|9@m;*I!>sBjOpl;SHbpeNN z2MU3pJrCTbR7-(UpP|~ScB=jAG4;5rQIDuzbqIHPT*naP#1jaRjR*vRlr6En^51ZJ_SAj zJ`B)|I|-JeU-DQE(C1$sFVNW0`#+C}iVISuw^_e>LYLwfkAD@A7)ALgI_XC{`gqd! z@%Jcs|0*a+P>FiEIt{!63yF@)FUkp~c=bAna z`aUw>^pRtQK60!SqhxL@-a!4l59_ zOFdfDs~gbL4d~Rx$JjTRhf+|7dOfnud?oQk@-k%wXM)`GQjWHwz2{J;FVr4=`55I4 zlxDt?dCU4j-vsoPK9j$J=+M>aRkXhbpRXYXE~M|FM7{@k{3b`1^*!6Tn}@d4AobV< zFmjxa=sV}1G)i|~;apOSIixN_r-OQ4;rzn$qsd)S(_Ofu<)3Z-qz*^Q9relF)uDyO zWz+>%bspwsW*@?R3lDXr2 zLLDXG^#OPQ#ro|7>~Tye^vLxRUvV{He)elp|@3S`<)gb54!hI$I?mJib-GP6{$mz={ zX9J=iDe0rs=>y-#WUSCXYKl3fR*Zhm2YvDUpKxAE_9b=ci+x}A+g$PcV$SBNE6C-Q zpfB>|c`4bK)TJ*R()Z*0u=F(nHQs?|#7DLxZziVE1b%qD5!e7Qu1A0t|GFCWesG@Wq-}xz z=VscK;g2mL8@f)s%~@D6?cUTVZF+6GdO=}LbCFUY^7+3$d;Q|6y+7;R`*LTGu~VIU zTszsh2li6uZeuTYcE8vGTd*wURPAQkJlNM$ zvsa~N)Td@P7T(naE;s)ALC3(%<=-t{2Kx?e=~LPc7cV_ryyS4vl0&q`2Yp-gWD)H^ z(ZU0TwEeW^ecJXGK47e;u&F2Aw1;*-Y`3=V^a5kM()aC3-S=(U{3lY4kEN=cQku)Y z(`z3*QnLI=G3bzyrsFaqop?ruPHv1c=m;YONOO6xw}fWs#4|Dh3Swje6vQZ#Q4pX9 z+!&bv?amC4W~8~a^p&(2IsqdSpddyjW%!LvnV&T`W&1Ky7Gy0dWroYq64D769g!Gi zG8!il`6=s8Yc8!vOJF{oKAkTepu9yCo3iFeSvx5+T>48|VIwdKG(DoM6N!kUEHh=A z%Zl%nuIMkdbb=VgipWpd7>NkuKHvg6@r*(uN;>XiW+KgH<@ZZrrR+(hJ$ zdq}Cr%)5q5h!InkgNTBudRgSSkDrOmR4wCP$7R((=_(x>pOgtue9A_ciElt$=7+`)P)5oeMuCFt+!#CVqeT>Y-b;)EE}BjpqwyD!=CT?@0via&C_&2d7LiVw z&!ueju`)|1uaw1)`y52HSjN5K@^sm1*s)AT!vd65L@hGPlD&awE^7=Ip%b673`V(# zDD?13SJt83+Uv`DB-xzDk8(>nR3HrIHPe9k$y$9Zy@sUn6|)8@aaS_x^*J5TtFuuMmdNm--n0h()oi5 znC9Xz(rOM8$ALD^B67Hp5kq6?#EZz+w8%QwaUb;tVjdov%UT^56P<_vjbJ zSY+dVGj!L6@mmv4vk2B{g}>=;$2qQ-53v>F8fD3v2{;qAr-KE}=icMbwE48K}fDgG*j1t`&s8E#|XclQHAA;L_8&l(_P^ z#FhC9$8{r)$$Cx3wh55+n*3JE@9wg$(v-3u(XPETYcO9yZUm>jP3UzaFdCo8ms@yE zrmH*pw-s0jkUQ5Pag8Fs15;Df9l^DvX5HwASWig|c@N`w7aFmTn`nM@6I`L9n!-}mh;e=PoD06G29leejFDfRHF_6a=iw$TmG_3PuuIx*Q~ z*?8@SH~;b5f1}_3)SR5??ytY2L!>pVmx5rka{?Pz0UWPgU0r$|2kY#!(m&4g)ZI>C zHh^X19&^Vwyq+a-5Aa`{W4^lpZsGRqr%kO>SbVO(%X40s1h5p{voEQ$f2p%yd?v=a K60?=~_5TA5L<>y- literal 0 HcmV?d00001 diff --git a/xmlDrawing.go b/xmlDrawing.go index d6d6135180..7d0b2df63b 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -118,7 +118,8 @@ const ( pivotTableVersion = 3 ) -var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff"} +// supportedImageTypes defined supported image types. +var supportedImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff", ".emf": ".emf"} // xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This // element specifies non-visual canvas properties. This allows for additional From c2311ce87dd2c681406728f885d2228dbefd7a21 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 14 May 2022 00:54:36 +0800 Subject: [PATCH 042/213] This made library allowing insert WMF format image --- picture.go | 2 +- picture_test.go | 2 ++ test/images/excel.wmf | Bin 0 -> 11208 bytes xmlDrawing.go | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 test/images/excel.wmf diff --git a/picture.go b/picture.go index f8133ca86f..8e2fa12f1e 100644 --- a/picture.go +++ b/picture.go @@ -366,7 +366,7 @@ func (f *File) addMedia(file []byte, ext string) string { // setContentTypePartImageExtensions provides a function to set the content // type for relationship parts and the Main Document part. func (f *File) setContentTypePartImageExtensions() { - imageTypes := map[string]string{"jpeg": "image/", "png": "image/", "gif": "image/", "tiff": "image/", "emf": "image/x-"} + imageTypes := map[string]string{"jpeg": "image/", "png": "image/", "gif": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-"} content := f.contentTypesReader() content.Lock() defer content.Unlock() diff --git a/picture_test.go b/picture_test.go index c5480352ac..60c6ac17cc 100644 --- a/picture_test.go +++ b/picture_test.go @@ -97,7 +97,9 @@ func TestAddPictureErrors(t *testing.T) { decode := func(r io.Reader) (image.Image, error) { return nil, nil } decodeConfig := func(r io.Reader) (image.Config, error) { return image.Config{Height: 100, Width: 90}, nil } image.RegisterFormat("emf", "", decode, decodeConfig) + image.RegisterFormat("wmf", "", decode, decodeConfig) assert.NoError(t, f.AddPicture("Sheet1", "Q1", filepath.Join("test", "images", "excel.emf"), "")) + assert.NoError(t, f.AddPicture("Sheet1", "Q7", filepath.Join("test", "images", "excel.wmf"), "")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx"))) assert.NoError(t, f.Close()) } diff --git a/test/images/excel.wmf b/test/images/excel.wmf new file mode 100644 index 0000000000000000000000000000000000000000..fd588c66f9e2fc1b62ffa766c15c2cf64c584f49 GIT binary patch literal 11208 zcmchdTWl0n7{|}-0;O(y&X!)dX{AbqAeRDiD}r1LLIJNK8oXkVDq290LaP)hLLgD2 zZ$20kZx2K?M4%8vK=i?A;)^jR1``uW@QDZ9Ap+ueIcH{eIy>E&-DUHAJDch3&TszT z|2t=zIlun&{rf_6$;+7@HR;MIOTcT$W0oivBKcb_yom*sxNiVt(i$uV!Zww-oavJI zsUk7k8f@X;23ZMF2HR^1A%?A)0iSU11o*wdu>CiINArvKTRHY8=zEteOH6{pXAZPP zt!PkfHfFC}t@Au=&)di4pY}<4!9Fc7+UMlolpX4C8?#GYw7b;>d$;=2?osD;9)#^d z`mJ14NO#G(7Lo(@FtcG3Vj}EoOqPZW&*fFK;iGj90h1tKFux(R#02;bs#gQ*UCv;h zgBoL=mj7f<$}5@U@>-@>3Q!WWiLybpgF5Z@<7yXW4}9iqf^8H0?Q$mg%$Ba3aX~KS z#*=czr}C?o?wTKWH@q4`_hfc83}-^z z1DuV?(o_0Xt$TQK3{@Mos%fFXmzd>F9)iy%Pr&D|DO2jCU8zpkQXPZiztY)m7pB^1 zQmt?bzbdl5Ifg1DwJIr4g}q{YQ?90i3Ye^vQ4NnPOo0)kK(sUZueN#_js>`P0*q(4 zy#zK5<}!}8#27dZ$F(Y9YFq}j9;8m$1J!Bh2+m%48oCSfd&=vu#kACS&{^l8d(MzQ zg3o5gvi`c~E?{X)7Kg;I&X5IorBT(Hs?`|^nNypjItNDoP~DJ6jH6Ugnl)vpPU^-&D*t_mBNh7X0*O4@3#Mwpk1xzy^`ug~GyAVzPQxpS>Lt8~!%iCxoyOxw61JR&V0*|;fKP77BlcB!6y~vG zlwO~(2iZ5qN9ZZ^7JAJ14S&BdwWgC=H;2@(Zut_Nm!hicddqmncqs`ZHYclX)OSbo zLPu}otvF})U%gt6_vulu-kq~dN16K}b022Zz%IjX!{Z9`h?%pZoY8-^sd2+&Tx|n< z2Rli^-5}T09i6#5O!r_XVMk$SVTW-R+8v|d{Ef-@XZNd6HVYjTzvia4rq)IcqmJ7F zg*x5TJjxl9aBcN;bz`g>H}JpTg=&=e5%xCG|9wKdBle3%yGxFRefXbX6Qm`E!MhOi z;FQ%VPTB!)Hwt?VAD1dh+%_4mg;75K=B-Efji0kk&3P5$-mMf{)V*8O>@9NkD;Uk2 zb@lxeiiSq3GfRX4W)@9Mo zArx3CCxT06Z0!uyk^6o#&1FGDwbq4M;JpgM!GRCKM14$zeV@nV;ZCqYBN4GI?jEvO zOQQwKDw&7KagaMUhW351iG1_D(a2#ToG_x&n}}Fi0K|!%evM+3EV7BHWSXVArMue2 ziBz)izR$x%&8F{}Wmk0zM0Yk(AA$Nc(LB>-@$Nu~WeKwwobZxm5#?hd-{fKLiJXzh zxoI>kfRi^-%)al}M9lI=4YTwmDv62c7R|i_F_Qk5iI`CK`h4A6@Em?{Co>*4AIkXicKcdi*6=EX80ysqE;$)Ys9}_Ljr)2$&M9i`mL7eak zlucAx6A{bGy+a{Zc!(mDtav8!uxOlkM4=|4zRx+GIU|u~8MbmS1ghf`=w_OTSoVLp zZq+`2n3PfsZ!MEZ_D93_!RO^@u7Ur z#ykRRL@+;;%P9}T7W0%`YoC$pbUtUFmCwT3%BLxRhQDL}q4Nj$KIR|rdze4#JR>{d z`v4nfa8$C|7>yUxn0h+nRPe`U}knDI$#ANrdidoji6b9ge!~qT!Uccq*?ulYeq47b>yvZ#MQ*_SOj>zLyPOm zMB Date: Sun, 15 May 2022 15:38:40 +0800 Subject: [PATCH 043/213] This closed #1163, fix set cell value with column and row style inherit issue --- cell.go | 10 +++++----- cell_test.go | 15 +++++++++++++++ styles.go | 4 ++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/cell.go b/cell.go index 80c03efe05..1e130dcba0 100644 --- a/cell.go +++ b/cell.go @@ -1145,6 +1145,11 @@ func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int { if style != 0 { return style } + if row <= len(ws.SheetData.Row) { + if styleID := ws.SheetData.Row[row-1].S; styleID != 0 { + return styleID + } + } if ws.Cols != nil { for _, c := range ws.Cols.Col { if c.Min <= col && col <= c.Max && c.Style != 0 { @@ -1152,11 +1157,6 @@ func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int { } } } - if row <= len(ws.SheetData.Row) { - if styleID := ws.SheetData.Row[row-1].S; styleID != 0 { - return styleID - } - } return style } diff --git a/cell_test.go b/cell_test.go index 8ed8e1f6c9..da251cdf13 100644 --- a/cell_test.go +++ b/cell_test.go @@ -156,6 +156,21 @@ func TestSetCellValue(t *testing.T) { f := NewFile() assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Duration(1e13)), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + // Test set cell value with column and row style inherit + style1, err := f.NewStyle(&Style{NumFmt: 2}) + assert.NoError(t, err) + style2, err := f.NewStyle(&Style{NumFmt: 9}) + assert.NoError(t, err) + assert.NoError(t, f.SetColStyle("Sheet1", "B", style1)) + assert.NoError(t, f.SetRowStyle("Sheet1", 1, 1, style2)) + assert.NoError(t, f.SetCellValue("Sheet1", "B1", 0.5)) + assert.NoError(t, f.SetCellValue("Sheet1", "B2", 0.5)) + B1, err := f.GetCellValue("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "50%", B1) + B2, err := f.GetCellValue("Sheet1", "B2") + assert.NoError(t, err) + assert.Equal(t, "0.50", B2) } func TestSetCellValues(t *testing.T) { diff --git a/styles.go b/styles.go index 6ef7dcbe20..b7b1525dce 100644 --- a/styles.go +++ b/styles.go @@ -901,7 +901,7 @@ func formatToC(v, format string, date1904 bool) string { if err != nil { return v } - f = f * 100 + f *= 100 return fmt.Sprintf("%.f%%", f) } @@ -912,7 +912,7 @@ func formatToD(v, format string, date1904 bool) string { if err != nil { return v } - f = f * 100 + f *= 100 return fmt.Sprintf("%.2f%%", f) } From be5a4033c0c7de6247c02dc3ab76b634ac19d4c6 Mon Sep 17 00:00:00 2001 From: sceneq Date: Mon, 16 May 2022 22:05:22 +0900 Subject: [PATCH 044/213] This closes #1229, rename ErrMaxFileNameLength to ErrMaxFilePathLength (#1230) Co-authored-by: sceneq --- errors.go | 4 ++-- excelize_test.go | 2 +- file.go | 4 ++-- xmlDrawing.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/errors.go b/errors.go index c9d18cb39f..64fc711666 100644 --- a/errors.go +++ b/errors.go @@ -109,9 +109,9 @@ var ( // ErrWorkbookExt defined the error message on receive an unsupported // workbook extension. ErrWorkbookExt = errors.New("unsupported workbook extension") - // ErrMaxFileNameLength defined the error message on receive the file name + // ErrMaxFilePathLength defined the error message on receive the file path // length overflow. - ErrMaxFileNameLength = errors.New("file name length exceeds maximum limit") + ErrMaxFilePathLength = errors.New("file path length exceeds maximum limit") // ErrEncrypt defined the error message on encryption spreadsheet. ErrEncrypt = errors.New("not support encryption currently") // ErrUnknownEncryptMechanism defined the error message on unsupported diff --git a/excelize_test.go b/excelize_test.go index 389573e296..27badc6c62 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -170,7 +170,7 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, f.SetCellStr("Sheet2", "c"+strconv.Itoa(i), strconv.Itoa(i))) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestOpenFile.xlsx"))) - assert.EqualError(t, f.SaveAs(filepath.Join("test", strings.Repeat("c", 199), ".xlsx")), ErrMaxFileNameLength.Error()) + assert.EqualError(t, f.SaveAs(filepath.Join("test", strings.Repeat("c", 199), ".xlsx")), ErrMaxFilePathLength.Error()) assert.NoError(t, f.Close()) } diff --git a/file.go b/file.go index 9707a7939c..1d3360e13d 100644 --- a/file.go +++ b/file.go @@ -66,8 +66,8 @@ func (f *File) Save() error { // SaveAs provides a function to create or update to a spreadsheet at the // provided path. func (f *File) SaveAs(name string, opt ...Options) error { - if len(name) > MaxFileNameLength { - return ErrMaxFileNameLength + if len(name) > MaxFilePathLength { + return ErrMaxFilePathLength } f.Path = name contentType, ok := map[string]string{ diff --git a/xmlDrawing.go b/xmlDrawing.go index 0b6df05f87..db5d750035 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -103,7 +103,7 @@ const ( StreamChunkSize = 1 << 24 MaxFontFamilyLength = 31 MaxFontSize = 409 - MaxFileNameLength = 207 + MaxFilePathLength = 207 MaxFieldLength = 255 MaxColumnWidth = 255 MaxRowHeight = 409 From 8f16a76781fb8f47094492c38a02c2cdc4ce5013 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 18 May 2022 23:15:24 +0800 Subject: [PATCH 045/213] This fixes a part of staticcheck issues and updates the code of conduct Update example for set cell hyperlinks with `HyperlinkOpts` --- CODE_OF_CONDUCT.md | 71 +++++++++++++++++++++++++++++++++------------- CONTRIBUTING.md | 16 +++++------ calc.go | 33 ++++----------------- cell.go | 6 +++- numfmt.go | 12 ++++---- picture.go | 4 +-- styles.go | 12 +++----- 7 files changed, 79 insertions(+), 75 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 572b5612e0..5c400f8ba3 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,45 +2,76 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment include: +Examples of behavior that contributes to a positive environment for our community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Publishing others’ private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [xuri.me](https://xuri.me). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [xuri.me](https://xuri.me). All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +Community Impact: A violation through a single incident or series of actions. + +Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +Community Impact: A serious violation of community standards, including sustained inappropriate behavior. + +Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +Consequence: A permanent ban from any sort of public interaction within the community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct][version] +This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). + +Community Impact Guidelines were inspired by Mozilla’s code of conduct enforcement ladder. -[homepage]: https://www.contributor-covenant.org -[version]: https://www.contributor-covenant.org/version/2/0/code_of_conduct +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89bc60edc4..847e3ac68d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -191,20 +191,18 @@ indicate acceptance. The sign-off is a simple line at the end of the explanation for the patch. Your signature certifies that you wrote the patch or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify -the below (from [developercertificate.org](http://developercertificate.org/)): +the below (from [developercertificate.org](https://developercertificate.org)): ```text Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. -1 Letterman Drive -Suite D4700 -San Francisco, CA, 94129 Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: @@ -347,9 +345,9 @@ The rules: 1. All code should be formatted with `gofmt -s`. 2. All code should pass the default levels of - [`golint`](https://github.com/golang/lint). + [`go vet`](https://pkg.go.dev/cmd/vet). 3. All code should follow the guidelines covered in [Effective - Go](http://golang.org/doc/effective_go.html) and [Go Code Review + Go](https://go.dev/doc/effective_go) and [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments). 4. Comment the code. Tell us the why, the history and the context. 5. Document _all_ declarations and methods, even private ones. Declare @@ -372,13 +370,13 @@ The rules: guidelines. Since you've read all the rules, you now know that. If you are having trouble getting into the mood of idiomatic Go, we recommend -reading through [Effective Go](https://golang.org/doc/effective_go.html). The -[Go Blog](https://blog.golang.org) is also a great resource. Drinking the +reading through [Effective Go](https://go.dev/doc/effective_go). The +[Go Blog](https://go.dev/blog/) is also a great resource. Drinking the kool-aid is a lot easier than going thirsty. ## Code Review Comments and Effective Go Guidelines -[CodeLingo](https://codelingo.io) automatically checks every pull request against the following guidelines from [Effective Go](https://golang.org/doc/effective_go.html) and [Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments). +[CodeLingo](https://www.codelingo.io) automatically checks every pull request against the following guidelines from [Effective Go](https://go.dev/doc/effective_go) and [Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments). ### Package Comment diff --git a/calc.go b/calc.go index 16d183b69c..f71f3e81e6 100644 --- a/calc.go +++ b/calc.go @@ -2512,31 +2512,23 @@ func convertTemperature(fromUOM, toUOM string, value float64) float64 { switch fromUOM { case "F": value = (value-32)/1.8 + 273.15 - break case "C": value += 273.15 - break case "Rank": value /= 1.8 - break case "Reau": value = value*1.25 + 273.15 - break } // convert from Kelvin switch toUOM { case "F": value = (value-273.15)*1.8 + 32 - break case "C": value -= 273.15 - break case "Rank": value *= 1.8 - break case "Reau": value = (value - 273.15) * 0.8 - break } return value } @@ -2567,8 +2559,8 @@ func (fn *formulaFuncs) CONVERT(argsList *list.List) formulaArg { } else if fromCategory == catgoryTemperature { return newNumberFormulaArg(convertTemperature(fromUOM, toUOM, val)) } - fromConversion, _ := unitConversions[fromCategory][fromUOM] - toConversion, _ := unitConversions[fromCategory][toUOM] + fromConversion := unitConversions[fromCategory][fromUOM] + toConversion := unitConversions[fromCategory][toUOM] baseValue := val * (1 / fromConversion) return newNumberFormulaArg((baseValue * toConversion) / toMultiplier) } @@ -4617,9 +4609,7 @@ func cofactorMatrix(i, j int, A [][]float64) float64 { sign = 1 } var B [][]float64 - for _, row := range A { - B = append(B, row) - } + B = append(B, A...) for m := 0; m < N; m++ { for n := j + 1; n < N; n++ { B[m][n-1] = B[m][n] @@ -8317,7 +8307,6 @@ func calcColumnMeans(mtxX, mtxRes [][]float64, c, r int) { } putDouble(mtxRes, i, sum/float64(r)) } - return } // calcColumnsDelta calculates subtract of the columns of matrix. @@ -8688,10 +8677,8 @@ func calcTrendGrowthRegression(bConstant, bGrowth bool, trendType, nCXN, nRXN, K switch trendType { case 1: calcTrendGrowthSimpleRegression(bConstant, bGrowth, mtxY, mtxX, newX, mtxRes, meanY, N) - break case 2: calcTrendGrowthMultipleRegressionPart1(bConstant, bGrowth, mtxY, mtxX, newX, mtxRes, meanY, nRXN, K, N) - break default: calcTrendGrowthMultipleRegressionPart2(bConstant, bGrowth, mtxY, mtxX, newX, mtxRes, meanY, nCXN, K, N) } @@ -8728,10 +8715,8 @@ func calcTrendGrowth(mtxY, mtxX, newX [][]float64, bConstant, bGrowth bool) ([][ switch trendType { case 1: mtxRes = getNewMatrix(nCXN, nRXN) - break case 2: mtxRes = getNewMatrix(1, nRXN) - break default: mtxRes = getNewMatrix(nCXN, 1) } @@ -10630,13 +10615,10 @@ func getTDist(T, fDF, nType float64) float64 { switch nType { case 1: res = 0.5 * getBetaDist(fDF/(fDF+T*T), fDF/2, 0.5) - break case 2: res = getBetaDist(fDF/(fDF+T*T), fDF/2, 0.5) - break case 3: res = math.Pow(1+(T*T/fDF), -(fDF+1)/2) / (math.Sqrt(fDF) * getBeta(0.5, fDF/2.0)) - break case 4: X := fDF / (T*T + fDF) R := 0.5 * getBetaDist(X, 0.5*fDF, 0.5) @@ -10644,7 +10626,6 @@ func getTDist(T, fDF, nType float64) float64 { if T < 0 { res = R } - break } return res } @@ -10888,15 +10869,11 @@ func tTest(bTemplin bool, mtx1, mtx2 [][]formulaArg, c1, c2, r1, r2 int, fT, fF return 0, 0, false } c := fS1 / (fS1 + fS2) - fT = math.Abs(sum1/cnt1-sum2/cnt2) / math.Sqrt(fS1+fS2) - fF = 1 / (c*c/(cnt1-1) + (1-c)*(1-c)/(cnt2-1)) - return fT, fF, true + return math.Abs(sum1/cnt1-sum2/cnt2) / math.Sqrt(fS1+fS2), 1 / (c*c/(cnt1-1) + (1-c)*(1-c)/(cnt2-1)), true } fS1 := (sumSqr1 - sum1*sum1/cnt1) / (cnt1 - 1) fS2 := (sumSqr2 - sum2*sum2/cnt2) / (cnt2 - 1) - fT = math.Abs(sum1/cnt1-sum2/cnt2) / math.Sqrt((cnt1-1)*fS1+(cnt2-1)*fS2) * math.Sqrt(cnt1*cnt2*(cnt1+cnt2-2)/(cnt1+cnt2)) - fF = cnt1 + cnt2 - 2 - return fT, fF, true + return math.Abs(sum1/cnt1-sum2/cnt2) / math.Sqrt((cnt1-1)*fS1+(cnt2-1)*fS2) * math.Sqrt(cnt1*cnt2*(cnt1+cnt2-2)/(cnt1+cnt2)), cnt1 + cnt2 - 2, true } // tTest is an implementation of the formula function TTEST. diff --git a/cell.go b/cell.go index 1e130dcba0..a302017795 100644 --- a/cell.go +++ b/cell.go @@ -686,8 +686,12 @@ type HyperlinkOpts struct { // the other functions such as `SetCellStyle` or `SetSheetRow`. The below is // example for external link. // +// display, tooltip := "https://github.com/xuri/excelize", "Excelize on GitHub" // if err := f.SetCellHyperLink("Sheet1", "A3", -// "https://github.com/xuri/excelize", "External"); err != nil { +// "https://github.com/xuri/excelize", "External", excelize.HyperlinkOpts{ +// Display: &display, +// Tooltip: &tooltip, +// }); err != nil { // fmt.Println(err) // } // // Set underline and font color style for the cell. diff --git a/numfmt.go b/numfmt.go index 2052fd9b5d..6b4fc65399 100644 --- a/numfmt.go +++ b/numfmt.go @@ -31,12 +31,12 @@ type languageInfo struct { // numberFormat directly maps the number format parser runtime required // fields. type numberFormat struct { - section []nfp.Section - t time.Time - sectionIdx int - date1904, isNumeric, hours, seconds bool - number float64 - ap, afterPoint, beforePoint, localCode, result, value, valueSectionType string + section []nfp.Section + t time.Time + sectionIdx int + date1904, isNumeric, hours, seconds bool + number float64 + ap, localCode, result, value, valueSectionType string } var ( diff --git a/picture.go b/picture.go index 8e2fa12f1e..30a66d36f3 100644 --- a/picture.go +++ b/picture.go @@ -371,9 +371,7 @@ func (f *File) setContentTypePartImageExtensions() { content.Lock() defer content.Unlock() for _, file := range content.Defaults { - if _, ok := imageTypes[file.Extension]; ok { - delete(imageTypes, file.Extension) - } + delete(imageTypes, file.Extension) } for extension, prefix := range imageTypes { content.Defaults = append(content.Defaults, xlsxDefault{ diff --git a/styles.go b/styles.go index b7b1525dce..4b5c77271a 100644 --- a/styles.go +++ b/styles.go @@ -874,11 +874,9 @@ func formatToA(v, format string, date1904 bool) string { return v } if f < 0 { - t := int(math.Abs(f)) - return fmt.Sprintf("(%d)", t) + return fmt.Sprintf("(%d)", int(math.Abs(f))) } - t := int(f) - return fmt.Sprintf("%d", t) + return fmt.Sprintf("%d", int(f)) } // formatToB provides a function to convert original string to special format @@ -901,8 +899,7 @@ func formatToC(v, format string, date1904 bool) string { if err != nil { return v } - f *= 100 - return fmt.Sprintf("%.f%%", f) + return fmt.Sprintf("%.f%%", f*100) } // formatToD provides a function to convert original string to special format @@ -912,8 +909,7 @@ func formatToD(v, format string, date1904 bool) string { if err != nil { return v } - f *= 100 - return fmt.Sprintf("%.2f%%", f) + return fmt.Sprintf("%.2f%%", f*100) } // formatToE provides a function to convert original string to special format From 63adac25897f295ef4493e060d917650f03ebd3b Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 20 May 2022 20:46:29 +0800 Subject: [PATCH 046/213] make workbook open filed exception message clear - New exported constant `ErrWorkbookPassword` - Rename exported constant `ErrWorkbookExt` to `ErrWorkbookFileFormat` --- crypt_test.go | 3 +++ errors.go | 9 ++++++--- excelize.go | 8 +++++--- excelize_test.go | 9 +++++---- file.go | 2 +- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/crypt_test.go b/crypt_test.go index 80f6cc4bcc..d09517674a 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -19,6 +19,9 @@ import ( ) func TestEncrypt(t *testing.T) { + _, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "passwd"}) + assert.EqualError(t, err, ErrWorkbookPassword.Error()) + f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"}) assert.NoError(t, err) assert.EqualError(t, f.SaveAs(filepath.Join("test", "BadEncrypt.xlsx"), Options{Password: "password"}), ErrEncrypt.Error()) diff --git a/errors.go b/errors.go index 64fc711666..61a7456859 100644 --- a/errors.go +++ b/errors.go @@ -106,9 +106,9 @@ var ( // ErrImgExt defined the error message on receive an unsupported image // extension. ErrImgExt = errors.New("unsupported image extension") - // ErrWorkbookExt defined the error message on receive an unsupported - // workbook extension. - ErrWorkbookExt = errors.New("unsupported workbook extension") + // ErrWorkbookFileFormat defined the error message on receive an + // unsupported workbook file format. + ErrWorkbookFileFormat = errors.New("unsupported workbook file format") // ErrMaxFilePathLength defined the error message on receive the file path // length overflow. ErrMaxFilePathLength = errors.New("file path length exceeds maximum limit") @@ -191,4 +191,7 @@ var ( // ErrSparklineStyle defined the error message on receive the invalid // sparkline Style parameters. ErrSparklineStyle = errors.New("parameter 'Style' must between 0-35") + // ErrWorkbookPassword defined the error message on receiving the incorrect + // workbook password. + ErrWorkbookPassword = errors.New("the supplied open workbook password is not correct") ) diff --git a/excelize.go b/excelize.go index d78d2b1ea5..0e2f440ed5 100644 --- a/excelize.go +++ b/excelize.go @@ -158,13 +158,15 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { return nil, ErrOptionsUnzipSizeLimit } if bytes.Contains(b, oleIdentifier) { - b, err = Decrypt(b, f.options) - if err != nil { - return nil, fmt.Errorf("decrypted file failed") + if b, err = Decrypt(b, f.options); err != nil { + return nil, ErrWorkbookFileFormat } } zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) if err != nil { + if len(f.options.Password) > 0 { + return nil, ErrWorkbookPassword + } return nil, err } file, sheetCount, err := f.ReadZipReader(zr) diff --git a/excelize_test.go b/excelize_test.go index 27badc6c62..f1b9903cbb 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1,6 +1,7 @@ package excelize import ( + "archive/zip" "bytes" "compress/gzip" "encoding/xml" @@ -179,7 +180,7 @@ func TestSaveFile(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - assert.EqualError(t, f.SaveAs(filepath.Join("test", "TestSaveFile.xlsb")), ErrWorkbookExt.Error()) + assert.EqualError(t, f.SaveAs(filepath.Join("test", "TestSaveFile.xlsb")), ErrWorkbookFileFormat.Error()) for _, ext := range []string{".xlam", ".xlsm", ".xlsx", ".xltm", ".xltx"} { assert.NoError(t, f.SaveAs(filepath.Join("test", fmt.Sprintf("TestSaveFile%s", ext)))) } @@ -207,9 +208,9 @@ func TestCharsetTranscoder(t *testing.T) { func TestOpenReader(t *testing.T) { _, err := OpenReader(strings.NewReader("")) - assert.EqualError(t, err, "zip: not a valid zip file") + assert.EqualError(t, err, zip.ErrFormat.Error()) _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password", UnzipXMLSizeLimit: UnzipSizeLimit + 1}) - assert.EqualError(t, err, "decrypted file failed") + assert.EqualError(t, err, ErrWorkbookFileFormat.Error()) // Test open spreadsheet with unzip size limit. _, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipSizeLimit: 100}) @@ -261,7 +262,7 @@ func TestOpenReader(t *testing.T) { 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x41, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, 0x00, 0x00, })) - assert.EqualError(t, err, "zip: unsupported compression algorithm") + assert.EqualError(t, err, zip.ErrAlgorithm.Error()) } func TestBrokenFile(t *testing.T) { diff --git a/file.go b/file.go index 1d3360e13d..ecdadf4c0b 100644 --- a/file.go +++ b/file.go @@ -78,7 +78,7 @@ func (f *File) SaveAs(name string, opt ...Options) error { ".xltx": ContentTypeTemplate, }[filepath.Ext(f.Path)] if !ok { - return ErrWorkbookExt + return ErrWorkbookFileFormat } f.setContentTypePartProjectExtensions(contentType) file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) From afb2d27c90130878b82a70b44ccb4e30344cc09e Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 23 May 2022 13:02:11 +0800 Subject: [PATCH 047/213] This fix formula calculation accuracy issue and panic when set pane - Fix `GROWTH` and `TREND` calculation accuracy issue - Fix panic when add pane on empty sheet views worksheet - New exported constants `MinFontSize` --- calc.go | 3 +-- calc_test.go | 6 +++--- errors.go | 8 ++++---- sheet.go | 3 +++ sheet_test.go | 6 ++++++ styles.go | 2 +- xmlDrawing.go | 1 + 7 files changed, 19 insertions(+), 10 deletions(-) diff --git a/calc.go b/calc.go index f71f3e81e6..8a80fb5f04 100644 --- a/calc.go +++ b/calc.go @@ -4579,8 +4579,7 @@ func newFormulaArgMatrix(numMtx [][]float64) (arg [][]formulaArg) { for r, row := range numMtx { arg = append(arg, make([]formulaArg, len(row))) for c, cell := range row { - decimal, _ := big.NewFloat(cell).SetPrec(15).Float64() - arg[r][c] = newNumberFormulaArg(decimal) + arg[r][c] = newNumberFormulaArg(cell) } } return diff --git a/calc_test.go b/calc_test.go index 205f329bdf..2cbcc97b11 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4518,17 +4518,17 @@ func TestCalcGROWTHandTREND(t *testing.T) { formulaList := map[string]string{ "=GROWTH(A2:B2)": "1", "=GROWTH(B2:B5,A2:A5,A8:A10)": "160", - "=GROWTH(B2:B5,A2:A5,A8:A10,FALSE)": "467.84375", + "=GROWTH(B2:B5,A2:A5,A8:A10,FALSE)": "467.842838114059", "=GROWTH(A4:A5,A2:B3,A8:A10,FALSE)": "", "=GROWTH(A3:A5,A2:B4,A2:B3)": "2", "=GROWTH(A4:A5,A2:B3)": "", "=GROWTH(A2:B2,A2:B3)": "", - "=GROWTH(A2:B2,A2:B3,A2:B3,FALSE)": "1.28399658203125", + "=GROWTH(A2:B2,A2:B3,A2:B3,FALSE)": "1.28402541668774", "=GROWTH(A2:B2,A4:B5,A4:B5,FALSE)": "1", "=GROWTH(A3:C3,A2:C3,A2:B3)": "2", "=TREND(A2:B2)": "1", "=TREND(B2:B5,A2:A5,A8:A10)": "95", - "=TREND(B2:B5,A2:A5,A8:A10,FALSE)": "81.66796875", + "=TREND(B2:B5,A2:A5,A8:A10,FALSE)": "81.6666666666667", "=TREND(A4:A5,A2:B3,A8:A10,FALSE)": "", "=TREND(A4:A5,A2:B3,A2:B3,FALSE)": "1.5", "=TREND(A3:A5,A2:B4,A2:B3)": "2", diff --git a/errors.go b/errors.go index 61a7456859..980629314e 100644 --- a/errors.go +++ b/errors.go @@ -102,7 +102,7 @@ var ( ErrMaxRows = errors.New("row number exceeds maximum limit") // ErrMaxRowHeight defined the error message on receive an invalid row // height. - ErrMaxRowHeight = errors.New("the height of the row must be smaller than or equal to 409 points") + ErrMaxRowHeight = fmt.Errorf("the height of the row must be smaller than or equal to %d points", MaxRowHeight) // ErrImgExt defined the error message on receive an unsupported image // extension. ErrImgExt = errors.New("unsupported image extension") @@ -145,9 +145,9 @@ var ( ErrCustomNumFmt = errors.New("custom number format can not be empty") // ErrFontLength defined the error message on the length of the font // family name overflow. - ErrFontLength = errors.New("the length of the font family name must be smaller than or equal to 31") + ErrFontLength = fmt.Errorf("the length of the font family name must be smaller than or equal to %d", MaxFontFamilyLength) // ErrFontSize defined the error message on the size of the font is invalid. - ErrFontSize = errors.New("font size must be between 1 and 409 points") + ErrFontSize = fmt.Errorf("font size must be between %d and %d points", MinFontSize, MaxFontSize) // ErrSheetIdx defined the error message on receive the invalid worksheet // index. ErrSheetIdx = errors.New("invalid worksheet index") @@ -161,7 +161,7 @@ var ( ErrGroupSheets = errors.New("group worksheet must contain an active worksheet") // ErrDataValidationFormulaLength defined the error message for receiving a // data validation formula length that exceeds the limit. - ErrDataValidationFormulaLength = errors.New("data validation must be 0-255 characters") + ErrDataValidationFormulaLength = fmt.Errorf("data validation must be 0-%d characters", MaxFieldLength) // ErrDataValidationRange defined the error message on set decimal range // exceeds limit. ErrDataValidationRange = errors.New("data validation range exceeds limit") diff --git a/sheet.go b/sheet.go index 4665fd9157..1c17f7829f 100644 --- a/sheet.go +++ b/sheet.go @@ -773,6 +773,9 @@ func (f *File) SetPanes(sheet, panes string) error { if fs.Freeze { p.State = "frozen" } + if ws.SheetViews == nil { + ws.SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{{}}} + } ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = p if !(fs.Freeze) && !(fs.Split) { if len(ws.SheetViews.SheetView) > 0 { diff --git a/sheet_test.go b/sheet_test.go index db36417812..3ad0e75245 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -104,6 +104,12 @@ func TestSetPane(t *testing.T) { assert.NoError(t, f.SetPanes("Panes 4", "")) assert.EqualError(t, f.SetPanes("SheetN", ""), "sheet SheetN is not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) + // Test add pane on empty sheet views worksheet + f = NewFile() + f.checked = nil + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(``)) + assert.NoError(t, f.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`)) } func TestPageLayoutOption(t *testing.T) { diff --git a/styles.go b/styles.go index 4b5c77271a..f8f4030489 100644 --- a/styles.go +++ b/styles.go @@ -2042,7 +2042,7 @@ func (f *File) getFontID(styleSheet *xlsxStyleSheet, style *Style) (fontID int) // settings. func (f *File) newFont(style *Style) *xlsxFont { fontUnderlineType := map[string]string{"single": "single", "double": "double"} - if style.Font.Size < 1 { + if style.Font.Size < MinFontSize { style.Font.Size = 11 } if style.Font.Color == "" { diff --git a/xmlDrawing.go b/xmlDrawing.go index db5d750035..480868593a 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -107,6 +107,7 @@ const ( MaxFieldLength = 255 MaxColumnWidth = 255 MaxRowHeight = 409 + MinFontSize = 1 TotalRows = 1048576 TotalColumns = 16384 TotalSheetHyperlinks = 65529 From 1c167b96a35a58990f777025914283b492d0785f Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 26 May 2022 00:15:28 +0800 Subject: [PATCH 048/213] Improves the calculation engine, docs update, and adds the dependabot - Initialize array formula support for the formula calculation engine - Update example and unit test of `AddPivotTable` - Update the supported hash algorithm of ProtectSheet --- .github/dependabot.yml | 10 ++++++ calc.go | 40 +++++++++++++++++++---- calc_test.go | 73 +++++++++++++++++++++++++++++++----------- pivotTable.go | 12 +++---- pivotTable_test.go | 12 +++---- sheet.go | 10 +++--- 6 files changed, 114 insertions(+), 43 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..dfefea14f7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: github-actions + directory: / + schedule: + interval: monthly +- package-ecosystem: gomod + directory: / + schedule: + interval: monthly diff --git a/calc.go b/calc.go index 8a80fb5f04..f636d7ff54 100644 --- a/calc.go +++ b/calc.go @@ -829,6 +829,8 @@ func newEmptyFormulaArg() formulaArg { func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, error) { 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] @@ -841,6 +843,14 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, // function start if isFunctionStartToken(token) { + if token.TValue == "ARRAY" { + inArray = true + continue + } + if token.TValue == "ARRAYROW" { + inArrayRow = true + continue + } opfStack.Push(token) argsStack.Push(list.New().Init()) opftStack.Push(token) // to know which operators belong to a function use the function as a separator @@ -922,7 +932,19 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeLogical { argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(token.TValue)) } - + if inArrayRow && isOperand(token) { + arrayRow = append(arrayRow, tokenToFormulaArg(token)) + continue + } + if inArrayRow && isFunctionStopToken(token) { + inArrayRow = false + continue + } + if inArray && isFunctionStopToken(token) { + argsStack.Peek().(*list.List).PushBack(opfdStack.Pop()) + arrayRow, inArray = []formulaArg{}, false + continue + } if err = f.evalInfixExpFunc(sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { return newEmptyFormulaArg(), err } @@ -1274,6 +1296,15 @@ func isOperand(token efp.Token) bool { return token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText) } +// tokenToFormulaArg create a formula argument by given token. +func tokenToFormulaArg(token efp.Token) formulaArg { + if token.TSubType == efp.TokenSubTypeNumber { + num, _ := strconv.ParseFloat(token.TValue, 64) + return newNumberFormulaArg(num) + } + return newStringFormulaArg(token.TValue) +} + // parseToken parse basic arithmetic operator priority and evaluate based on // operators and operands. func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error { @@ -1318,12 +1349,7 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta } // opd if isOperand(token) { - if token.TSubType == efp.TokenSubTypeNumber { - num, _ := strconv.ParseFloat(token.TValue, 64) - opdStack.Push(newNumberFormulaArg(num)) - } else { - opdStack.Push(newStringFormulaArg(token.TValue)) - } + opdStack.Push(tokenToFormulaArg(token)) } return nil } diff --git a/calc_test.go b/calc_test.go index 2cbcc97b11..b71e93bb58 100644 --- a/calc_test.go +++ b/calc_test.go @@ -60,55 +60,88 @@ func TestCalcCellValue(t *testing.T) { "=1&2": "12", "=15%": "0.15", "=1+20%": "1.2", + "={1}+2": "3", + "=1+{2}": "3", + "={1}+{2}": "3", `="A"="A"`: "TRUE", `="A"<>"A"`: "FALSE", // Engineering Functions // BESSELI - "=BESSELI(4.5,1)": "15.3892227537359", - "=BESSELI(32,1)": "5502845511211.25", + "=BESSELI(4.5,1)": "15.3892227537359", + "=BESSELI(32,1)": "5502845511211.25", + "=BESSELI({32},1)": "5502845511211.25", + "=BESSELI(32,{1})": "5502845511211.25", + "=BESSELI({32},{1})": "5502845511211.25", // BESSELJ - "=BESSELJ(1.9,2)": "0.329925727692387", + "=BESSELJ(1.9,2)": "0.329925727692387", + "=BESSELJ({1.9},2)": "0.329925727692387", + "=BESSELJ(1.9,{2})": "0.329925727692387", + "=BESSELJ({1.9},{2})": "0.329925727692387", // BESSELK - "=BESSELK(0.05,0)": "3.11423403428966", - "=BESSELK(0.05,1)": "19.9096743272486", - "=BESSELK(0.05,2)": "799.501207124235", - "=BESSELK(3,2)": "0.0615104585619118", + "=BESSELK(0.05,0)": "3.11423403428966", + "=BESSELK(0.05,1)": "19.9096743272486", + "=BESSELK(0.05,2)": "799.501207124235", + "=BESSELK(3,2)": "0.0615104585619118", + "=BESSELK({3},2)": "0.0615104585619118", + "=BESSELK(3,{2})": "0.0615104585619118", + "=BESSELK({3},{2})": "0.0615104585619118", // BESSELY - "=BESSELY(0.05,0)": "-1.97931100684153", - "=BESSELY(0.05,1)": "-12.789855163794", - "=BESSELY(0.05,2)": "-509.61489554492", - "=BESSELY(9,2)": "-0.229082087487741", + "=BESSELY(0.05,0)": "-1.97931100684153", + "=BESSELY(0.05,1)": "-12.789855163794", + "=BESSELY(0.05,2)": "-509.61489554492", + "=BESSELY(9,2)": "-0.229082087487741", + "=BESSELY({9},2)": "-0.229082087487741", + "=BESSELY(9,{2})": "-0.229082087487741", + "=BESSELY({9},{2})": "-0.229082087487741", // BIN2DEC "=BIN2DEC(\"10\")": "2", "=BIN2DEC(\"11\")": "3", "=BIN2DEC(\"0000000010\")": "2", "=BIN2DEC(\"1111111110\")": "-2", "=BIN2DEC(\"110\")": "6", + "=BIN2DEC({\"110\"})": "6", // BIN2HEX "=BIN2HEX(\"10\")": "2", "=BIN2HEX(\"0000000001\")": "1", "=BIN2HEX(\"10\",10)": "0000000002", "=BIN2HEX(\"1111111110\")": "FFFFFFFFFE", "=BIN2HEX(\"11101\")": "1D", + "=BIN2HEX({\"11101\"})": "1D", // BIN2OCT "=BIN2OCT(\"101\")": "5", "=BIN2OCT(\"0000000001\")": "1", "=BIN2OCT(\"10\",10)": "0000000002", "=BIN2OCT(\"1111111110\")": "7777777776", "=BIN2OCT(\"1110\")": "16", + "=BIN2OCT({\"1110\"})": "16", // BITAND - "=BITAND(13,14)": "12", + "=BITAND(13,14)": "12", + "=BITAND({13},14)": "12", + "=BITAND(13,{14})": "12", + "=BITAND({13},{14})": "12", // BITLSHIFT - "=BITLSHIFT(5,2)": "20", - "=BITLSHIFT(3,5)": "96", + "=BITLSHIFT(5,2)": "20", + "=BITLSHIFT({3},5)": "96", + "=BITLSHIFT(3,5)": "96", + "=BITLSHIFT(3,{5})": "96", + "=BITLSHIFT({3},{5})": "96", // BITOR - "=BITOR(9,12)": "13", + "=BITOR(9,12)": "13", + "=BITOR({9},12)": "13", + "=BITOR(9,{12})": "13", + "=BITOR({9},{12})": "13", // BITRSHIFT - "=BITRSHIFT(20,2)": "5", - "=BITRSHIFT(52,4)": "3", + "=BITRSHIFT(20,2)": "5", + "=BITRSHIFT(52,4)": "3", + "=BITRSHIFT({52},4)": "3", + "=BITRSHIFT(52,{4})": "3", + "=BITRSHIFT({52},{4})": "3", // BITXOR - "=BITXOR(5,6)": "3", - "=BITXOR(9,12)": "5", + "=BITXOR(5,6)": "3", + "=BITXOR(9,12)": "5", + "=BITXOR({9},12)": "5", + "=BITXOR(9,{12})": "5", + "=BITXOR({9},{12})": "5", // COMPLEX "=COMPLEX(5,2)": "5+2i", "=COMPLEX(5,-9)": "5-9i", @@ -221,6 +254,7 @@ func TestCalcCellValue(t *testing.T) { "=HEX2OCT(\"8\",10)": "0000000010", "=HEX2OCT(\"FFFFFFFFF8\")": "7777777770", "=HEX2OCT(\"1F3\")": "763", + "=HEX2OCT({\"1F3\"})": "763", // IMABS "=IMABS(\"2j\")": "2", "=IMABS(\"-1+2i\")": "2.23606797749979", @@ -773,6 +807,7 @@ func TestCalcCellValue(t *testing.T) { "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.6666666666667", "=SUM(1+ROW())": "2", "=SUM((SUM(2))+1)": "3", + "=SUM({1,2,3,4,\"\"})": "10", // SUMIF `=SUMIF(F1:F5, "")`: "0", `=SUMIF(A1:A5, "3")`: "3", diff --git a/pivotTable.go b/pivotTable.go index 28c863290f..de671f756c 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -102,12 +102,12 @@ type PivotTableField struct { // types := []string{"Meat", "Dairy", "Beverages", "Produce"} // region := []string{"East", "West", "North", "South"} // f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}) -// for i := 0; i < 30; i++ { -// f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)]) -// f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)]) -// f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)]) -// f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000)) -// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)]) +// for row := 2; row < 32; row++ { +// f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), year[rand.Intn(3)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000)) +// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]) // } // if err := f.AddPivotTable(&excelize.PivotTableOption{ // DataRange: "Sheet1!$A$1:$E$31", diff --git a/pivotTable_test.go b/pivotTable_test.go index 2f95ed4986..adba2eb197 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -18,12 +18,12 @@ func TestAddPivotTable(t *testing.T) { types := []string{"Meat", "Dairy", "Beverages", "Produce"} region := []string{"East", "West", "North", "South"} assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"})) - for i := 0; i < 30; i++ { - assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)])) - assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)])) - assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)])) - assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000))) - assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)])) + for row := 2; row < 32; row++ { + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)])) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), year[rand.Intn(3)])) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)])) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000))) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)])) } assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", diff --git a/sheet.go b/sheet.go index 1c17f7829f..47a206387f 100644 --- a/sheet.go +++ b/sheet.go @@ -1069,12 +1069,12 @@ func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error return err } -// ProtectSheet provides a function to prevent other users from accidentally -// or deliberately changing, moving, or deleting data in a worksheet. The +// ProtectSheet provides a function to prevent other users from accidentally or +// deliberately changing, moving, or deleting data in a worksheet. The // optional field AlgorithmName specified hash algorithm, support XOR, MD4, -// MD5, SHA1, SHA256, SHA384, and SHA512 currently, if no hash algorithm -// specified, will be using the XOR algorithm as default. For example, -// protect Sheet1 with protection settings: +// MD5, SHA-1, SHA2-56, SHA-384, and SHA-512 currently, if no hash algorithm +// specified, will be using the XOR algorithm as default. For example, protect +// Sheet1 with protection settings: // // err := f.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ // AlgorithmName: "SHA-512", From 551b83afab85f2911410a6fa994fe5cc883d8804 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 May 2022 13:01:49 +0800 Subject: [PATCH 049/213] This update dependencies module and GitHub Action settings Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/dependabot.yml | 5 +---- .github/workflows/go.yml | 2 +- go.mod | 11 +++++++---- go.sum | 25 ++++++++++++++----------- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dfefea14f7..e49472e656 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,4 @@ updates: directory: / schedule: interval: monthly -- package-ecosystem: gomod - directory: / - schedule: - interval: monthly + \ No newline at end of file diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index bc5db46b9b..a81d4044da 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -31,7 +31,7 @@ jobs: run: env GO111MODULE=on go test -v -timeout 30m -race ./... -coverprofile=coverage.txt -covermode=atomic - name: Codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: file: coverage.txt flags: unittests diff --git a/go.mod b/go.mod index 116b7a1725..b08e3d209f 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,16 @@ module github.com/xuri/excelize/v2 go 1.15 require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.4 - github.com/stretchr/testify v1.7.0 + 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/nfp v0.0.0-20220409054826-5e722a1d9e22 - golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 - golang.org/x/image v0.0.0-20211028202545-6944b10bf410 - golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e + golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 + golang.org/x/net v0.0.0-20220524220425-1d687d428aca golang.org/x/text v0.3.7 + gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index 8ca6233f05..db6f6ad1a5 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,29 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= -github.com/richardlehane/msoleps v1.0.1 h1:RfrALnSNXzmXLbGct/P2b4xkFz4e8Gmj/0Vj9M9xC1o= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= +github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/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-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM= -golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +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/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-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= -golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +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/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= @@ -34,5 +36,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 7a6d5f5ebe5fa9b74ec58b79baf79b369d496e21 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 29 May 2022 19:37:10 +0800 Subject: [PATCH 050/213] This initialized support for encryption workbook by password, ref #199 - Remove exported variable `ErrEncrypt` --- crypt.go | 907 ++++++++++++++++++++++++++++++++++++++++++-------- crypt_test.go | 19 +- errors.go | 2 - excelize.go | 4 - file.go | 3 + 5 files changed, 780 insertions(+), 155 deletions(-) diff --git a/crypt.go b/crypt.go index da9feb4a65..65d5dae2b9 100644 --- a/crypt.go +++ b/crypt.go @@ -15,7 +15,6 @@ import ( "bytes" "crypto/aes" "crypto/cipher" - "crypto/hmac" "crypto/md5" "crypto/rand" "crypto/sha1" @@ -25,6 +24,7 @@ import ( "encoding/binary" "encoding/xml" "hash" + "math" "reflect" "strings" @@ -36,17 +36,11 @@ import ( var ( blockKey = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption - blockKeyHmacKey = []byte{0x5f, 0xb2, 0xad, 0x01, 0x0c, 0xb9, 0xe1, 0xf6} - blockKeyHmacValue = []byte{0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, 0x33} - blockKeyVerifierHashInput = []byte{0xfe, 0xa7, 0xd2, 0x76, 0x3b, 0x4b, 0x9e, 0x79} - blockKeyVerifierHashValue = []byte{0xd7, 0xaa, 0x0f, 0x6d, 0x30, 0x61, 0x34, 0x4e} - packageOffset = 8 // First 8 bytes are the size of the stream - packageEncryptionChunkSize = 4096 + oleIdentifier = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1} iterCount = 50000 + packageEncryptionChunkSize = 4096 + packageOffset = 8 // First 8 bytes are the size of the stream sheetProtectionSpinCount = 1e5 - oleIdentifier = []byte{ - 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1, - } ) // Encryption specifies the encryption structure, streams, and storages are @@ -128,6 +122,14 @@ type StandardEncryptionVerifier struct { EncryptedVerifierHash []byte } +// encryptionInfo structure is used for standard encryption with SHA1 +// cryptographic algorithm. +type encryption struct { + BlockSize, SaltSize int + EncryptedKeyValue, EncryptedVerifierHashInput, EncryptedVerifierHashValue, SaltValue []byte + KeyBits uint32 +} + // Decrypt API decrypts the CFB file format with ECMA-376 agile encryption and // standard encryption. Support cryptographic algorithm: MD4, MD5, RIPEMD-160, // SHA1, SHA256, SHA384 and SHA512 currently. @@ -154,125 +156,27 @@ func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { // Encrypt API encrypt data with the password. func Encrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { - // Generate a random key to use to encrypt the document. Excel uses 32 bytes. We'll use the password to encrypt this key. - packageKey, _ := randomBytes(32) - keyDataSaltValue, _ := randomBytes(16) - keyEncryptors, _ := randomBytes(16) - encryptionInfo := Encryption{ - KeyData: KeyData{ - BlockSize: 16, - KeyBits: len(packageKey) * 8, - HashSize: 64, - CipherAlgorithm: "AES", - CipherChaining: "ChainingModeCBC", - HashAlgorithm: "SHA512", - SaltValue: base64.StdEncoding.EncodeToString(keyDataSaltValue), - }, - KeyEncryptors: KeyEncryptors{ - KeyEncryptor: []KeyEncryptor{{ - EncryptedKey: EncryptedKey{ - SpinCount: 100000, KeyData: KeyData{ - CipherAlgorithm: "AES", - CipherChaining: "ChainingModeCBC", - HashAlgorithm: "SHA512", - HashSize: 64, - BlockSize: 16, - KeyBits: 256, - SaltValue: base64.StdEncoding.EncodeToString(keyEncryptors), - }, - }, - }}, - }, - } - - // Package Encryption - - // Encrypt package using the package key. - encryptedPackage, err := cryptPackage(true, packageKey, raw, encryptionInfo) - if err != nil { - return - } - - // Data Integrity - - // Create the data integrity fields used by clients for integrity checks. - // Generate a random array of bytes to use in HMAC. The docs say to use the same length as the key salt, but Excel seems to use 64. - hmacKey, _ := randomBytes(64) - if err != nil { - return - } - // Create an initialization vector using the package encryption info and the appropriate block key. - hmacKeyIV, err := createIV(blockKeyHmacKey, encryptionInfo) - if err != nil { - return + encryptor := encryption{ + EncryptedVerifierHashInput: make([]byte, 16), + EncryptedVerifierHashValue: make([]byte, 32), + SaltValue: make([]byte, 16), + BlockSize: 16, + KeyBits: 128, + SaltSize: 16, } - // Use the package key and the IV to encrypt the HMAC key. - encryptedHmacKey, _ := crypt(true, encryptionInfo.KeyData.CipherAlgorithm, encryptionInfo.KeyData.CipherChaining, packageKey, hmacKeyIV, hmacKey) - // Create the HMAC. - h := hmac.New(sha512.New, append(hmacKey, encryptedPackage...)) - for _, buf := range [][]byte{hmacKey, encryptedPackage} { - _, _ = h.Write(buf) - } - hmacValue := h.Sum(nil) - // Generate an initialization vector for encrypting the resulting HMAC value. - hmacValueIV, err := createIV(blockKeyHmacValue, encryptionInfo) - if err != nil { - return - } - // Encrypt the value. - encryptedHmacValue, _ := crypt(true, encryptionInfo.KeyData.CipherAlgorithm, encryptionInfo.KeyData.CipherChaining, packageKey, hmacValueIV, hmacValue) - // Put the encrypted key and value on the encryption info. - encryptionInfo.DataIntegrity.EncryptedHmacKey = base64.StdEncoding.EncodeToString(encryptedHmacKey) - encryptionInfo.DataIntegrity.EncryptedHmacValue = base64.StdEncoding.EncodeToString(encryptedHmacValue) - // Key Encryption - - // Convert the password to an encryption key. - key, err := convertPasswdToKey(opt.Password, blockKey, encryptionInfo) - if err != nil { - return - } - // Encrypt the package key with the encryption key. - encryptedKeyValue, _ := crypt(true, encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.CipherAlgorithm, encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.CipherChaining, key, keyEncryptors, packageKey) - encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.EncryptedKeyValue = base64.StdEncoding.EncodeToString(encryptedKeyValue) - - // Verifier hash - - // Create a random byte array for hashing. - verifierHashInput, _ := randomBytes(16) - // Create an encryption key from the password for the input. - verifierHashInputKey, err := convertPasswdToKey(opt.Password, blockKeyVerifierHashInput, encryptionInfo) - if err != nil { - return - } - // Use the key to encrypt the verifier input. - encryptedVerifierHashInput, err := crypt(true, encryptionInfo.KeyData.CipherAlgorithm, encryptionInfo.KeyData.CipherChaining, verifierHashInputKey, keyEncryptors, verifierHashInput) - if err != nil { - return - } - encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.EncryptedVerifierHashInput = base64.StdEncoding.EncodeToString(encryptedVerifierHashInput) - // Create a hash of the input. - verifierHashValue := hashing(encryptionInfo.KeyData.HashAlgorithm, verifierHashInput) - // Create an encryption key from the password for the hash. - verifierHashValueKey, err := convertPasswdToKey(opt.Password, blockKeyVerifierHashValue, encryptionInfo) - if err != nil { - return - } - // Use the key to encrypt the hash value. - encryptedVerifierHashValue, err := crypt(true, encryptionInfo.KeyData.CipherAlgorithm, encryptionInfo.KeyData.CipherChaining, verifierHashValueKey, keyEncryptors, verifierHashValue) + encryptionInfoBuffer, err := encryptor.standardKeyEncryption(opt.Password) if err != nil { - return - } - encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.EncryptedVerifierHashValue = base64.StdEncoding.EncodeToString(encryptedVerifierHashValue) - // Marshal the encryption info buffer. - encryptionInfoBuffer, err := xml.Marshal(encryptionInfo) - if err != nil { - return + return nil, err } - // TODO: Create a new CFB. - _, _ = encryptedPackage, encryptionInfoBuffer - err = ErrEncrypt - return + // Package Encryption + encryptedPackage := make([]byte, 8) + binary.LittleEndian.PutUint64(encryptedPackage, uint64(len(raw))) + encryptedPackage = append(encryptedPackage, encryptor.encrypt(raw)...) + // Create a new CFB + compoundFile := cfb{} + packageBuf = compoundFile.Writer(encryptionInfoBuffer, encryptedPackage) + return packageBuf, nil } // extractPart extract data from storage by specified part name. @@ -419,6 +323,68 @@ func standardXORBytes(a, b []byte) []byte { return buf } +// encrypt provides a function to encrypt given value with AES cryptographic +// algorithm. +func (e *encryption) encrypt(input []byte) []byte { + inputBytes := len(input) + if pad := inputBytes % e.BlockSize; pad != 0 { + inputBytes += e.BlockSize - pad + } + var output, chunk []byte + encryptedChunk := make([]byte, e.BlockSize) + for i := 0; i < inputBytes; i += e.BlockSize { + if i+e.BlockSize <= len(input) { + chunk = input[i : i+e.BlockSize] + } else { + chunk = input[i:] + } + chunk = append(chunk, make([]byte, e.BlockSize-len(chunk))...) + c, _ := aes.NewCipher(e.EncryptedKeyValue) + c.Encrypt(encryptedChunk, chunk) + output = append(output, encryptedChunk...) + } + return output +} + +// standardKeyEncryption encrypt convert the password to an encryption key. +func (e *encryption) standardKeyEncryption(password string) ([]byte, error) { + if len(password) == 0 || len(password) > MaxFieldLength { + return nil, ErrPasswordLengthInvalid + } + var storage cfb + storage.writeUint16(0x0003) + storage.writeUint16(0x0002) + storage.writeUint32(0x24) + storage.writeUint32(0xA4) + storage.writeUint32(0x24) + storage.writeUint32(0x00) + storage.writeUint32(0x660E) + storage.writeUint32(0x8004) + storage.writeUint32(0x80) + storage.writeUint32(0x18) + storage.writeUint64(0x00) + providerName := "Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)" + storage.writeStrings(providerName) + storage.writeUint16(0x00) + storage.writeUint32(0x10) + keyDataSaltValue, _ := randomBytes(16) + verifierHashInput, _ := randomBytes(16) + e.SaltValue = keyDataSaltValue + e.EncryptedKeyValue, _ = standardConvertPasswdToKey( + StandardEncryptionHeader{KeySize: e.KeyBits}, + StandardEncryptionVerifier{Salt: e.SaltValue}, + &Options{Password: password}) + verifierHashInputKey := hashing("sha1", verifierHashInput) + e.EncryptedVerifierHashInput = e.encrypt(verifierHashInput) + e.EncryptedVerifierHashValue = e.encrypt(verifierHashInputKey) + storage.writeBytes(e.SaltValue) + storage.writeBytes(e.EncryptedVerifierHashInput) + storage.writeUint32(0x14) + storage.writeBytes(e.EncryptedVerifierHashValue) + storage.position = 0 + return storage.stream, nil +} + // ECMA-376 Agile Encryption // agileDecrypt decrypt the CFB file format with ECMA-376 agile encryption. @@ -444,9 +410,9 @@ func agileDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options) ( if err != nil { return } - packageKey, _ := crypt(false, encryptedKey.CipherAlgorithm, encryptedKey.CipherChaining, key, saltValue, encryptedKeyValue) + packageKey, _ := decrypt(key, saltValue, encryptedKeyValue) // Use the package key to decrypt the package. - return cryptPackage(false, packageKey, encryptedPackageBuf, encryptionInfo) + return decryptPackage(packageKey, encryptedPackageBuf, encryptionInfo) } // convertPasswdToKey convert the password into an encryption key. @@ -519,30 +485,21 @@ func parseEncryptionInfo(encryptionInfo []byte) (encryption Encryption, err erro return } -// crypt encrypt / decrypt input by given cipher algorithm, cipher chaining, -// key and initialization vector. -func crypt(encrypt bool, cipherAlgorithm, cipherChaining string, key, iv, input []byte) (packageKey []byte, err error) { +// decrypt provides a function to decrypt input by given cipher algorithm, +// cipher chaining, key and initialization vector. +func decrypt(key, iv, input []byte) (packageKey []byte, err error) { block, err := aes.NewCipher(key) if err != nil { return input, err } - var stream cipher.BlockMode - if encrypt { - stream = cipher.NewCBCEncrypter(block, iv) - } else { - stream = cipher.NewCBCDecrypter(block, iv) - } - stream.CryptBlocks(input, input) + cipher.NewCBCDecrypter(block, iv).CryptBlocks(input, input) return input, nil } -// cryptPackage encrypt / decrypt package by given packageKey and encryption +// decryptPackage decrypt package by given packageKey and encryption // info. -func cryptPackage(encrypt bool, packageKey, input []byte, encryption Encryption) (outputChunks []byte, err error) { +func decryptPackage(packageKey, input []byte, encryption Encryption) (outputChunks []byte, err error) { encryptedKey, offset := encryption.KeyData, packageOffset - if encrypt { - offset = 0 - } var i, start, end int var iv, outputChunk []byte for end < len(input) { @@ -570,17 +527,14 @@ func cryptPackage(encrypt bool, packageKey, input []byte, encryption Encryption) if err != nil { return } - // Encrypt/decrypt the chunk and add it to the array - outputChunk, err = crypt(encrypt, encryptedKey.CipherAlgorithm, encryptedKey.CipherChaining, packageKey, iv, inputChunk) + // Decrypt the chunk and add it to the array + outputChunk, err = decrypt(packageKey, iv, inputChunk) if err != nil { return } outputChunks = append(outputChunks, outputChunk...) i++ } - if encrypt { - outputChunks = append(createUInt32LEBuffer(len(input), 8), outputChunks...) - } return } @@ -662,3 +616,662 @@ func genISOPasswdHash(passwd, hashAlgorithm, salt string, spinCount int) (hashVa hashValue, saltValue = base64.StdEncoding.EncodeToString(key), base64.StdEncoding.EncodeToString(s) return } + +// Compound File Binary Implements + +// cfb structure is used for the compound file binary (CFB) file format writer. +type cfb struct { + stream []byte + position int +} + +// writeBytes write bytes in the stream by a given value with an offset. +func (c *cfb) writeBytes(value []byte) { + pos := c.position + for i := 0; i < len(value); i++ { + for j := len(c.stream); j <= i+pos; j++ { + c.stream = append(c.stream, 0) + } + c.stream[i+pos] = value[i] + } + c.position = pos + len(value) +} + +// writeUint16 write an uint16 data type bytes in the stream by a given value +// with an offset. +func (c *cfb) writeUint16(value int) { + buf := make([]byte, 2) + binary.LittleEndian.PutUint16(buf, uint16(value)) + c.writeBytes(buf) +} + +// writeUint32 write an uint32 data type bytes in the stream by a given value +// with an offset. +func (c *cfb) writeUint32(value int) { + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, uint32(value)) + c.writeBytes(buf) +} + +// writeUint64 write an uint64 data type bytes in the stream by a given value +// with an offset. +func (c *cfb) writeUint64(value int) { + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, uint64(value)) + c.writeBytes(buf) +} + +// writeBytes write strings in the stream by a given value with an offset. +func (c *cfb) writeStrings(value string) { + encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() + buffer, err := encoder.Bytes([]byte(value)) + if err != nil { + return + } + c.writeBytes(buffer) +} + +// writeVersionStream provides a function to write compound file version +// stream. +func (c *cfb) writeVersionStream() []byte { + var storage cfb + storage.writeUint32(0x3c) + storage.writeStrings("Microsoft.Container.DataSpaces") + storage.writeUint32(0x01) + storage.writeUint32(0x01) + storage.writeUint32(0x01) + return storage.stream +} + +// writeDataSpaceMapStream provides a function to write compound file +// DataSpaceMap stream. +func (c *cfb) writeDataSpaceMapStream() []byte { + var storage cfb + storage.writeUint32(0x08) + storage.writeUint32(0x01) + storage.writeUint32(0x68) + storage.writeUint32(0x01) + storage.writeUint32(0x00) + storage.writeUint32(0x20) + storage.writeStrings("EncryptedPackage") + storage.writeUint32(0x32) + storage.writeStrings("StrongEncryptionDataSpace") + storage.writeUint16(0x00) + return storage.stream +} + +// writeStrongEncryptionDataSpaceStream provides a function to write compound +// file StrongEncryptionDataSpace stream. +func (c *cfb) writeStrongEncryptionDataSpaceStream() []byte { + var storage cfb + storage.writeUint32(0x08) + storage.writeUint32(0x01) + storage.writeUint32(0x32) + storage.writeStrings("StrongEncryptionTransform") + storage.writeUint16(0x00) + return storage.stream +} + +// writePrimaryStream provides a function to write compound file Primary +// stream. +func (c *cfb) writePrimaryStream() []byte { + var storage cfb + storage.writeUint32(0x6C) + storage.writeUint32(0x01) + storage.writeUint32(0x4C) + storage.writeStrings("{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}") + storage.writeUint32(0x4E) + storage.writeUint16(0x00) + storage.writeUint32(0x01) + storage.writeUint32(0x01) + storage.writeUint32(0x01) + storage.writeStrings("AES128") + storage.writeUint32(0x00) + storage.writeUint32(0x04) + return storage.stream +} + +// writeFileStream provides a function to write encrypted package in compound +// file by a given buffer and the short sector allocation table. +func (c *cfb) writeFileStream(encryptionInfoBuffer []byte, SSAT []int) ([]byte, []int) { + var ( + storage cfb + miniProperties int + stream = make([]byte, 0x100) + ) + if encryptionInfoBuffer != nil { + copy(stream, encryptionInfoBuffer) + } + storage.writeBytes(stream) + streamBlocks := len(stream) / 64 + if len(stream)%64 > 0 { + streamBlocks++ + } + for i := 1; i < streamBlocks; i++ { + SSAT = append(SSAT, i) + } + SSAT = append(SSAT, -2) + miniProperties += streamBlocks + versionStream := make([]byte, 0x80) + version := c.writeVersionStream() + copy(versionStream, version) + storage.writeBytes(versionStream) + versionBlocks := len(versionStream) / 64 + if len(versionStream)%64 > 0 { + versionBlocks++ + } + for i := 1; i < versionBlocks; i++ { + SSAT = append(SSAT, i+miniProperties) + } + SSAT = append(SSAT, -2) + miniProperties += versionBlocks + dataSpaceMap := make([]byte, 0x80) + dataStream := c.writeDataSpaceMapStream() + copy(dataSpaceMap, dataStream) + storage.writeBytes(dataSpaceMap) + dataSpaceMapBlocks := len(dataSpaceMap) / 64 + if len(dataSpaceMap)%64 > 0 { + dataSpaceMapBlocks++ + } + for i := 1; i < dataSpaceMapBlocks; i++ { + SSAT = append(SSAT, i+miniProperties) + } + SSAT = append(SSAT, -2) + miniProperties += dataSpaceMapBlocks + dataSpaceStream := c.writeStrongEncryptionDataSpaceStream() + storage.writeBytes(dataSpaceStream) + dataSpaceStreamBlocks := len(dataSpaceStream) / 64 + if len(dataSpaceStream)%64 > 0 { + dataSpaceStreamBlocks++ + } + for i := 1; i < dataSpaceStreamBlocks; i++ { + SSAT = append(SSAT, i+miniProperties) + } + SSAT = append(SSAT, -2) + miniProperties += dataSpaceStreamBlocks + primaryStream := make([]byte, 0x1C0) + primary := c.writePrimaryStream() + copy(primaryStream, primary) + storage.writeBytes(primaryStream) + primaryBlocks := len(primary) / 64 + if len(primary)%64 > 0 { + primaryBlocks++ + } + for i := 1; i < primaryBlocks; i++ { + SSAT = append(SSAT, i+miniProperties) + } + SSAT = append(SSAT, -2) + if len(SSAT) < 128 { + for i := len(SSAT); i < 128; i++ { + SSAT = append(SSAT, -1) + } + } + storage.position = 0 + return storage.stream, SSAT +} + +// writeRootEntry provides a function to write compound file root directory +// entry. The first entry in the first sector of the directory chain +// (also referred to as the first element of the directory array, or stream +// ID #0) is known as the root directory entry, and it is reserved for two +// purposes. First, it provides a root parent for all objects that are +// stationed at the root of the compound file. Second, its function is +// overloaded to store the size and starting sector for the mini stream. +func (c *cfb) writeRootEntry(customSectID int) []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("Root Entry") + storage.position = 0x40 + storage.writeUint16(0x16) + storage.writeBytes([]byte{5, 0}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(customSectID) + storage.writeUint32(0x340) + return storage.stream +} + +// writeEncryptionInfo provides a function to write compound file +// writeEncryptionInfo stream. The writeEncryptionInfo stream contains +// detailed information that is used to initialize the cryptography used to +// encrypt the EncryptedPackage stream. +func (c *cfb) writeEncryptionInfo() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("EncryptionInfo") + storage.position = 0x40 + storage.writeUint16(0x1E) + storage.writeBytes([]byte{2, 1}) + storage.writeUint32(0x03) + storage.writeUint32(0x02) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0xF8) + return storage.stream +} + +// writeEncryptedPackage provides a function to write compound file +// writeEncryptedPackage stream. The writeEncryptedPackage stream is an +// encrypted stream of bytes containing the entire ECMA-376 source file in +// compressed form. +func (c *cfb) writeEncryptedPackage(propertyCount, size int) []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("EncryptedPackage") + storage.position = 0x40 + storage.writeUint16(0x22) + storage.writeBytes([]byte{2, 0}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(propertyCount) + storage.writeUint32(size) + return storage.stream +} + +// writeDataSpaces provides a function to write compound file writeDataSpaces +// stream. The data spaces structure consists of a set of interrelated +// storages and streams in an OLE compound file. +func (c *cfb) writeDataSpaces() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeUint16(0x06) + storage.position = 0x40 + storage.writeUint16(0x18) + storage.writeBytes([]byte{1, 0}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(5) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + return storage.stream +} + +// writeVersion provides a function to write compound file version. The +// writeVersion structure specifies the version of a product or feature. It +// contains a major and a minor version number. +func (c *cfb) writeVersion() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("Version") + storage.position = 0x40 + storage.writeUint16(0x10) + storage.writeBytes([]byte{2, 1}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(4) + storage.writeUint32(76) + return storage.stream +} + +// writeDataSpaceMap provides a function to write compound file +// writeDataSpaceMap stream. The writeDataSpaceMap structure associates +// protected content with data space definitions. The data space definition, +// in turn, describes the series of transforms that MUST be applied to that +// protected content to restore it to its original form. By using a map to +// associate data space definitions with content, a single data space +// definition can be used to define the transforms applied to more than one +// piece of protected content. However, a given piece of protected content can +// be referenced only by a single data space definition. +func (c *cfb) writeDataSpaceMap() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("DataSpaceMap") + storage.position = 0x40 + storage.writeUint16(0x1A) + storage.writeBytes([]byte{2, 1}) + storage.writeUint32(0x04) + storage.writeUint32(0x06) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(6) + storage.writeUint32(112) + return storage.stream +} + +// writeDataSpaceInfo provides a function to write compound file +// writeDataSpaceInfo storage. The writeDataSpaceInfo is a storage containing +// the data space definitions used in the file. This storage must contain one +// or more streams, each of which contains a DataSpaceDefinition structure. +// The storage must contain exactly one stream for each DataSpaceMapEntry +// structure in the DataSpaceMap stream. The name of each stream must be equal +// to the DataSpaceName field of exactly one DataSpaceMapEntry structure +// contained in the DataSpaceMap stream. +func (c *cfb) writeDataSpaceInfo() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("DataSpaceInfo") + storage.position = 0x40 + storage.writeUint16(0x1C) + storage.writeBytes([]byte{1, 1}) + storage.writeUint32(-1) + storage.writeUint32(8) + storage.writeUint32(7) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + return storage.stream +} + +// writeStrongEncryptionDataSpace provides a function to write compound file +// writeStrongEncryptionDataSpace stream. +func (c *cfb) writeStrongEncryptionDataSpace() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("StrongEncryptionDataSpace") + storage.position = 0x40 + storage.writeUint16(0x34) + storage.writeBytes([]byte{2, 1}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(8) + storage.writeUint32(64) + return storage.stream +} + +// writeTransformInfo provides a function to write compound file +// writeTransformInfo storage. writeTransformInfo is a storage containing +// definitions for the transforms used in the data space definitions stored in +// the DataSpaceInfo storage. The stream contains zero or more definitions for +// the possible transforms that can be applied to the data in content +// streams. +func (c *cfb) writeTransformInfo() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("TransformInfo") + storage.position = 0x40 + storage.writeUint16(0x1C) + storage.writeBytes([]byte{1, 0}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(9) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + return storage.stream +} + +// writeStrongEncryptionTransform provides a function to write compound file +// writeStrongEncryptionTransform storage. +func (c *cfb) writeStrongEncryptionTransform() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("StrongEncryptionTransform") + storage.position = 0x40 + storage.writeUint16(0x34) + storage.writeBytes([]byte{1}) + storage.writeBytes([]byte{1}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(0x0A) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + return storage.stream +} + +// writePrimary provides a function to write compound file writePrimary stream. +func (c *cfb) writePrimary() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeUint16(0x06) + storage.writeStrings("Primary") + storage.position = 0x40 + storage.writeUint16(0x12) + storage.writeBytes([]byte{2, 1}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(9) + storage.writeUint32(208) + return storage.stream +} + +// writeNoneDir provides a function to write compound file writeNoneDir stream. +func (c *cfb) writeNoneDir() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.position = 0x40 + storage.writeUint16(0x00) + storage.writeUint16(0x00) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(-1) + return storage.stream +} + +// writeDirectoryEntry provides a function to write compound file directory +// entries. The directory entry array is an array of directory entries that +// are grouped into a directory sector. Each storage object or stream object +// within a compound file is represented by a single directory entry. The +// space for the directory sectors that are holding the array is allocated +// from the FAT. +func (c *cfb) writeDirectoryEntry(propertyCount, customSectID, size int) []byte { + var storage cfb + if size < 0 { + size = 0 + } + for _, entry := range [][]byte{ + c.writeRootEntry(customSectID), + c.writeEncryptionInfo(), + c.writeEncryptedPackage(propertyCount, size), + c.writeDataSpaces(), + c.writeVersion(), + c.writeDataSpaceMap(), + c.writeDataSpaceInfo(), + c.writeStrongEncryptionDataSpace(), + c.writeTransformInfo(), + c.writeStrongEncryptionTransform(), + c.writePrimary(), + c.writeNoneDir(), + } { + storage.writeBytes(entry) + } + return storage.stream +} + +// writeMSAT provides a function to write compound file sector allocation +// table. +func (c *cfb) writeMSAT(MSATBlocks, SATBlocks int, MSAT []int) []int { + if MSATBlocks > 0 { + cnt, MSATIdx := MSATBlocks*128+109, 0 + for i := 0; i < cnt; i++ { + if i < SATBlocks { + bufferSize := i - 109 + if bufferSize > 0 && bufferSize%0x80 == 0 { + MSATIdx++ + MSAT = append(MSAT, MSATIdx) + } + MSAT = append(MSAT, i+MSATBlocks) + continue + } + MSAT = append(MSAT, -1) + } + } else { + for i := 0; i < 109; i++ { + if i < SATBlocks { + MSAT = append(MSAT, i) + continue + } + MSAT = append(MSAT, -1) + } + } + return MSAT +} + +// writeSAT provides a function to write compound file master sector allocation +// table. +func (c *cfb) writeSAT(MSATBlocks, SATBlocks, SSATBlocks, directoryBlocks, fileBlocks, streamBlocks int, SAT []int) (int, []int) { + var blocks int + if SATBlocks > 0 { + for i := 1; i <= MSATBlocks; i++ { + SAT = append(SAT, -4) + } + blocks = MSATBlocks + for i := 1; i <= SATBlocks; i++ { + SAT = append(SAT, -3) + } + blocks += SATBlocks + for i := 1; i < SSATBlocks; i++ { + SAT = append(SAT, i) + } + SAT = append(SAT, -2) + blocks += SSATBlocks + for i := 1; i < directoryBlocks; i++ { + SAT = append(SAT, i+blocks) + } + SAT = append(SAT, -2) + blocks += directoryBlocks + for i := 1; i < fileBlocks; i++ { + SAT = append(SAT, i+blocks) + } + SAT = append(SAT, -2) + blocks += fileBlocks + for i := 1; i < streamBlocks; i++ { + SAT = append(SAT, i+blocks) + } + SAT = append(SAT, -2) + } + return blocks, SAT +} + +// Writer provides a function to create compound file with given info stream +// and package stream. +// +// MSAT - The master sector allocation table +// SSAT - The short sector allocation table +// SAT - The sector allocation table +// +func (c *cfb) Writer(encryptionInfoBuffer, encryptedPackage []byte) []byte { + var ( + storage cfb + MSAT, SAT, SSAT []int + directoryBlocks, fileBlocks, SSATBlocks = 3, 2, 1 + size = int(math.Max(float64(len(encryptedPackage)), float64(packageEncryptionChunkSize))) + streamBlocks = len(encryptedPackage) / 0x200 + ) + if len(encryptedPackage)%0x200 > 0 { + streamBlocks++ + } + propertyBlocks := directoryBlocks + fileBlocks + SSATBlocks + blockSize := (streamBlocks + propertyBlocks) * 4 + SATBlocks := blockSize / 0x200 + if blockSize%0x200 > 0 { + SATBlocks++ + } + MSATBlocks, blocksChanged := 0, true + for blocksChanged { + var SATCap, MSATCap int + blocksChanged = false + blockSize = (streamBlocks + propertyBlocks + SATBlocks + MSATBlocks) * 4 + SATCap = blockSize / 0x200 + if blockSize%0x200 > 0 { + SATCap++ + } + if SATCap > SATBlocks { + SATBlocks, blocksChanged = SATCap, true + continue + } + if SATBlocks > 109 { + blockRemains := (SATBlocks - 109) * 4 + blockBuffer := blockRemains % 0x200 + MSATCap = blockRemains / 0x200 + if blockBuffer > 0 { + MSATCap++ + } + if blockBuffer+(4*MSATCap) > 0x200 { + MSATCap++ + } + if MSATCap > MSATBlocks { + MSATBlocks, blocksChanged = MSATCap, true + } + } + } + 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) + storage.writeUint16(0x003E) + storage.writeUint16(0x0003) + storage.writeUint16(-2) + storage.writeUint16(9) + storage.writeUint32(6) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(SATBlocks) + storage.writeUint32(MSATBlocks + SATBlocks + SSATBlocks) + storage.writeUint32(0) + storage.writeUint32(0x00001000) + storage.writeUint32(SATBlocks + MSATBlocks) + storage.writeUint32(SSATBlocks) + if MSATBlocks > 0 { + storage.writeUint32(0) + storage.writeUint32(MSATBlocks) + } else { + storage.writeUint32(-2) + storage.writeUint32(0) + } + for _, block := range MSAT { + storage.writeUint32(block) + } + for i := 0; i < SATBlocks*128; i++ { + if i < len(SAT) { + storage.writeUint32(SAT[i]) + continue + } + storage.writeUint32(-1) + } + fileStream, SSATStream := c.writeFileStream(encryptionInfoBuffer, SSAT) + for _, block := range SSATStream { + storage.writeUint32(block) + } + directoryEntry := c.writeDirectoryEntry(blocks, blocks-fileBlocks, size) + storage.writeBytes(directoryEntry) + storage.writeBytes(fileStream) + storage.writeBytes(encryptedPackage) + return storage.stream +} diff --git a/crypt_test.go b/crypt_test.go index d09517674a..2df5af2cca 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -13,18 +13,33 @@ package excelize import ( "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" ) func TestEncrypt(t *testing.T) { + // Test decrypt spreadsheet with incorrect password _, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "passwd"}) assert.EqualError(t, err, ErrWorkbookPassword.Error()) - + // Test decrypt spreadsheet with password f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"}) assert.NoError(t, err) - assert.EqualError(t, f.SaveAs(filepath.Join("test", "BadEncrypt.xlsx"), Options{Password: "password"}), ErrEncrypt.Error()) + cell, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "SECRET", cell) + assert.NoError(t, f.Close()) + // Test encrypt spreadsheet with invalid password + assert.EqualError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: strings.Repeat("*", MaxFieldLength+1)}), ErrPasswordLengthInvalid.Error()) + // Test encrypt spreadsheet with new password + assert.NoError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: "passwd"})) + assert.NoError(t, f.Close()) + f, err = OpenFile(filepath.Join("test", "Encryption.xlsx"), Options{Password: "passwd"}) + assert.NoError(t, err) + cell, err = f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "SECRET", cell) assert.NoError(t, f.Close()) } diff --git a/errors.go b/errors.go index 980629314e..6606f1eaf5 100644 --- a/errors.go +++ b/errors.go @@ -112,8 +112,6 @@ var ( // ErrMaxFilePathLength defined the error message on receive the file path // length overflow. ErrMaxFilePathLength = errors.New("file path length exceeds maximum limit") - // ErrEncrypt defined the error message on encryption spreadsheet. - ErrEncrypt = errors.New("not support encryption currently") // ErrUnknownEncryptMechanism defined the error message on unsupported // encryption mechanism. ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism") diff --git a/excelize.go b/excelize.go index 0e2f440ed5..8c71b167e1 100644 --- a/excelize.go +++ b/excelize.go @@ -93,10 +93,6 @@ type Options struct { // return // } // -// Note that the excelize just support decrypt and not support encrypt -// currently, the spreadsheet saved by Save and SaveAs will be without -// password unprotected. Close the file by Close after opening the -// spreadsheet. func OpenFile(filename string, opt ...Options) (*File, error) { file, err := os.Open(filepath.Clean(filename)) if err != nil { diff --git a/file.go b/file.go index ecdadf4c0b..5931bdb4fb 100644 --- a/file.go +++ b/file.go @@ -60,6 +60,9 @@ func (f *File) Save() error { if f.Path == "" { return ErrSave } + if f.options != nil { + return f.SaveAs(f.Path, *f.options) + } return f.SaveAs(f.Path) } From 8fde918d981e9229b43fc6045b259a1db31cf1f4 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 31 May 2022 11:05:10 +0800 Subject: [PATCH 051/213] This update docs and tests for workbook encryption --- .gitignore | 2 +- crypt.go | 27 +++++++++++---------------- crypt_test.go | 13 +++++++++++++ excelize.go | 1 + stream_test.go | 2 ++ 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 4dce768053..e697544817 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ test/Test*.xlsx test/Test*.xltm test/Test*.xltx # generated files -test/BadEncrypt.xlsx +test/Encryption*.xlsx test/BadWorkbook.SaveAsEmptyStruct.xlsx test/*.png test/excelize-* diff --git a/crypt.go b/crypt.go index 65d5dae2b9..239208db1e 100644 --- a/crypt.go +++ b/crypt.go @@ -143,15 +143,10 @@ func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { if err != nil || mechanism == "extensible" { return } - switch mechanism { - case "agile": + if mechanism == "agile" { return agileDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt) - case "standard": - return standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt) - default: - err = ErrUnsupportedEncryptMechanism } - return + return standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt) } // Encrypt API encrypt data with the password. @@ -1112,7 +1107,7 @@ func (c *cfb) writeDirectoryEntry(propertyCount, customSectID, size int) []byte return storage.stream } -// writeMSAT provides a function to write compound file sector allocation +// writeMSAT provides a function to write compound file master sector allocation // table. func (c *cfb) writeMSAT(MSATBlocks, SATBlocks int, MSAT []int) []int { if MSATBlocks > 0 { @@ -1129,19 +1124,19 @@ func (c *cfb) writeMSAT(MSATBlocks, SATBlocks int, MSAT []int) []int { } MSAT = append(MSAT, -1) } - } else { - for i := 0; i < 109; i++ { - if i < SATBlocks { - MSAT = append(MSAT, i) - continue - } - MSAT = append(MSAT, -1) + return MSAT + } + for i := 0; i < 109; i++ { + if i < SATBlocks { + MSAT = append(MSAT, i) + continue } + MSAT = append(MSAT, -1) } return MSAT } -// writeSAT provides a function to write compound file master sector allocation +// writeSAT provides a function to write compound file sector allocation // table. func (c *cfb) writeSAT(MSATBlocks, SATBlocks, SSATBlocks, directoryBlocks, fileBlocks, streamBlocks int, SAT []int) (int, []int) { var blocks int diff --git a/crypt_test.go b/crypt_test.go index 2df5af2cca..d2fba35983 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -12,6 +12,7 @@ package excelize import ( + "io/ioutil" "path/filepath" "strings" "testing" @@ -30,6 +31,13 @@ func TestEncrypt(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "SECRET", cell) assert.NoError(t, f.Close()) + // Test decrypt spreadsheet with unsupported encrypt mechanism + raw, err := ioutil.ReadFile(filepath.Join("test", "encryptAES.xlsx")) + assert.NoError(t, err) + raw[2050] = 3 + _, err = Decrypt(raw, &Options{Password: "password"}) + assert.EqualError(t, err, ErrUnsupportedEncryptMechanism.Error()) + // Test encrypt spreadsheet with invalid password assert.EqualError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: strings.Repeat("*", MaxFieldLength+1)}), ErrPasswordLengthInvalid.Error()) // Test encrypt spreadsheet with new password @@ -51,6 +59,11 @@ func TestEncryptionMechanism(t *testing.T) { assert.EqualError(t, err, ErrUnknownEncryptMechanism.Error()) } +func TestEncryptionWriteDirectoryEntry(t *testing.T) { + cfb := cfb{} + assert.Equal(t, 1536, len(cfb.writeDirectoryEntry(0, 0, -1))) +} + func TestHashing(t *testing.T) { assert.Equal(t, hashing("unsupportedHashAlgorithm", []byte{}), []byte(nil)) } diff --git a/excelize.go b/excelize.go index 8c71b167e1..aaa4953817 100644 --- a/excelize.go +++ b/excelize.go @@ -93,6 +93,7 @@ type Options struct { // return // } // +// Close the file by Close function after opening the spreadsheet. func OpenFile(filename string, opt ...Options) (*File, error) { file, err := os.Open(filepath.Clean(filename)) if err != nil { diff --git a/stream_test.go b/stream_test.go index 3df898a701..9776b384a9 100644 --- a/stream_test.go +++ b/stream_test.go @@ -129,6 +129,8 @@ func TestStreamWriter(t *testing.T) { } assert.NoError(t, rows.Close()) assert.Equal(t, 2559558, cells) + // Save spreadsheet with password. + assert.NoError(t, file.SaveAs(filepath.Join("test", "EncryptionTestStreamWriter.xlsx"), Options{Password: "password"})) assert.NoError(t, file.Close()) } From 604a01bf6b3b1e0d95fe3501f6309d3ed78b900c Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 9 Jun 2022 08:16:48 +0800 Subject: [PATCH 052/213] ref #65: new formula function WEEKNUM --- calc.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 20 +++++++++++++ 2 files changed, 101 insertions(+) diff --git a/calc.go b/calc.go index f636d7ff54..3669e308d2 100644 --- a/calc.go +++ b/calc.go @@ -719,6 +719,7 @@ type formulaFuncs struct { // VDB // VLOOKUP // WEEKDAY +// WEEKNUM // WEIBULL // WEIBULL.DIST // XIRR @@ -12803,6 +12804,86 @@ func (fn *formulaFuncs) WEEKDAY(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } +// weeknum is an implementation of the formula function WEEKNUM. +func (fn *formulaFuncs) weeknum(snTime time.Time, returnType int) formulaArg { + days := snTime.YearDay() + weekMod, weekNum := days%7, math.Ceil(float64(days)/7) + if weekMod == 0 { + weekMod = 7 + } + year := snTime.Year() + firstWeekday := int(time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC).Weekday()) + var offset int + switch returnType { + case 1, 17: + offset = 0 + case 2, 11, 21: + offset = 1 + case 12, 13, 14, 15, 16: + offset = returnType - 10 + default: + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + padding := offset + 7 - firstWeekday + if padding > 7 { + padding -= 7 + } + if weekMod > padding { + weekNum++ + } + if returnType == 21 && (firstWeekday == 0 || firstWeekday > 4) { + if weekNum--; weekNum < 1 { + if weekNum = 52; int(time.Date(year-1, time.January, 1, 0, 0, 0, 0, time.UTC).Weekday()) < 4 { + weekNum++ + } + } + } + return newNumberFormulaArg(weekNum) +} + +// WEEKNUM function returns an integer representing the week number (from 1 to +// 53) of the year. The syntax of the function is: +// +// WEEKNUM(serial_number,[return_type]) +// +func (fn *formulaFuncs) WEEKNUM(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "WEEKNUM requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "WEEKNUM allows at most 2 arguments") + } + sn := argsList.Front().Value.(formulaArg) + num, returnType := sn.ToNumber(), 1 + var snTime time.Time + if num.Type != ArgNumber { + dateString := strings.ToLower(sn.Value()) + if !isDateOnlyFmt(dateString) { + if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError { + return err + } + } + y, m, d, _, err := strToDate(dateString) + if err.Type == ArgError { + return err + } + snTime = time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.Now().Location()) + } else { + if num.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + snTime = timeFromExcelTime(num.Number, false) + } + if argsList.Len() == 2 { + returnTypeArg := argsList.Back().Value.(formulaArg).ToNumber() + if returnTypeArg.Type != ArgNumber { + return returnTypeArg + } + returnType = int(returnTypeArg.Number) + } + return fn.weeknum(snTime, returnType) +} + // Text Functions // CHAR function returns the character relating to a supplied character set diff --git a/calc_test.go b/calc_test.go index b71e93bb58..874a3e5efe 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1560,6 +1560,18 @@ func TestCalcCellValue(t *testing.T) { "=WEEKDAY(\"12/25/2012\",15)": "5", "=WEEKDAY(\"12/25/2012\",16)": "4", "=WEEKDAY(\"12/25/2012\",17)": "3", + // WEEKNUM + "=WEEKNUM(\"01/01/2011\")": "1", + "=WEEKNUM(\"01/03/2011\")": "2", + "=WEEKNUM(\"01/13/2008\")": "3", + "=WEEKNUM(\"01/21/2008\")": "4", + "=WEEKNUM(\"01/30/2008\")": "5", + "=WEEKNUM(\"02/04/2008\")": "6", + "=WEEKNUM(\"01/02/2017\",2)": "2", + "=WEEKNUM(\"01/02/2017\",12)": "1", + "=WEEKNUM(\"12/31/2017\",21)": "52", + "=WEEKNUM(\"01/01/2017\",21)": "52", + "=WEEKNUM(\"01/01/2021\",21)": "53", // Text Functions // CHAR "=CHAR(65)": "A", @@ -3484,6 +3496,14 @@ func TestCalcCellValue(t *testing.T) { "=WEEKDAY(0,0)": "#VALUE!", "=WEEKDAY(\"January 25, 100\")": "#VALUE!", "=WEEKDAY(-1,1)": "#NUM!", + // WEEKNUM + "=WEEKNUM()": "WEEKNUM requires at least 1 argument", + "=WEEKNUM(0,1,0)": "WEEKNUM allows at most 2 arguments", + "=WEEKNUM(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=WEEKNUM(\"\",1)": "#VALUE!", + "=WEEKNUM(\"January 25, 100\")": "#VALUE!", + "=WEEKNUM(0,0)": "#NUM!", + "=WEEKNUM(-1,1)": "#NUM!", // Text Functions // CHAR "=CHAR()": "CHAR requires 1 argument", From 980fffa2b621e933ab16debf9d9106b005941589 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 10 Jun 2022 00:10:22 +0800 Subject: [PATCH 053/213] ref #65: new formula function EDATE --- calc.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++---- calc_test.go | 10 +++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index 3669e308d2..fc983e9945 100644 --- a/calc.go +++ b/calc.go @@ -433,6 +433,7 @@ type formulaFuncs struct { // DOLLARFR // DURATION // EFFECT +// EDATE // ENCODEURL // ERF // ERF.PRECISE @@ -1544,7 +1545,7 @@ func formulaCriteriaParser(exp string) (fc *formulaCriteria) { if exp == "" { return } - if match := regexp.MustCompile(`^([0-9]+)$`).FindStringSubmatch(exp); len(match) > 1 { + if match := regexp.MustCompile(`^(\d+)$`).FindStringSubmatch(exp); len(match) > 1 { fc.Type, fc.Condition = criteriaEq, match[1] return } @@ -10862,7 +10863,7 @@ func (fn *formulaFuncs) TREND(argsList *list.List) formulaArg { } // tTest calculates the probability associated with the Student's T Test. -func tTest(bTemplin bool, mtx1, mtx2 [][]formulaArg, c1, c2, r1, r2 int, fT, fF float64) (float64, float64, bool) { +func tTest(bTemplin bool, mtx1, mtx2 [][]formulaArg, c1, c2, r1, r2 int) (float64, float64, bool) { var cnt1, cnt2, sum1, sumSqr1, sum2, sumSqr2 float64 var fVal formulaArg for i := 0; i < c1; i++ { @@ -10935,9 +10936,9 @@ func (fn *formulaFuncs) tTest(mtx1, mtx2 [][]formulaArg, fTails, fTyp float64) f fT = math.Abs(sumD) * math.Sqrt((cnt-1)/divider) fF = cnt - 1 } else if fTyp == 2 { - fT, fF, ok = tTest(false, mtx1, mtx2, c1, c2, r1, r2, fT, fF) + fT, fF, ok = tTest(false, mtx1, mtx2, c1, c2, r1, r2) } else { - fT, fF, ok = tTest(true, mtx1, mtx2, c1, c2, r1, r2, fT, fF) + fT, fF, ok = tTest(true, mtx1, mtx2, c1, c2, r1, r2) } if !ok { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) @@ -12351,6 +12352,58 @@ func (fn *formulaFuncs) ISOWEEKNUM(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(weekNum)) } +// EDATE function returns a date that is a specified number of months before or +// after a supplied start date. The syntax of function is: +// +// EDATE(start_date,months) +// +func (fn *formulaFuncs) EDATE(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "EDATE requires 2 arguments") + } + date := argsList.Front().Value.(formulaArg) + num := date.ToNumber() + var dateTime time.Time + if num.Type != ArgNumber { + dateString := strings.ToLower(date.Value()) + if !isDateOnlyFmt(dateString) { + if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError { + return err + } + } + y, m, d, _, err := strToDate(dateString) + if err.Type == ArgError { + return err + } + dateTime = time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.Now().Location()) + } else { + if num.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + dateTime = timeFromExcelTime(num.Number, false) + } + month := argsList.Back().Value.(formulaArg).ToNumber() + if month.Type != ArgNumber { + return month + } + y, d := dateTime.Year(), dateTime.Day() + m := int(dateTime.Month()) + int(month.Number) + if month.Number < 0 { + y -= int(math.Ceil(-1 * float64(m) / 12)) + } + if month.Number > 11 { + y += int(math.Floor(float64(m) / 12)) + } + m = int(math.Mod(float64(m), 12)) + if d > 28 { + if days := getDaysInMonth(y, m); d > days { + d = days + } + } + result, _ := timeToExcelTime(time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC), false) + return newNumberFormulaArg(result) +} + // HOUR function returns an integer representing the hour component of a // supplied Excel time. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 874a3e5efe..ab282ca4f8 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1475,6 +1475,10 @@ func TestCalcCellValue(t *testing.T) { "=DAYS(2,1)": "1", "=DAYS(INT(2),INT(1))": "1", "=DAYS(\"02/02/2015\",\"01/01/2015\")": "32", + // EDATE + "=EDATE(\"01/01/2021\",-1)": "44166", + "=EDATE(\"01/31/2020\",1)": "43890", + "=EDATE(\"01/29/2020\",12)": "44225", // HOUR "=HOUR(1)": "0", "=HOUR(43543.5032060185)": "12", @@ -3437,6 +3441,12 @@ func TestCalcCellValue(t *testing.T) { "=DAYS(0,\"\")": "#VALUE!", "=DAYS(NA(),0)": "#VALUE!", "=DAYS(0,NA())": "#VALUE!", + // EDATE + "=EDATE()": "EDATE requires 2 arguments", + "=EDATE(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=EDATE(-1,0)": "#NUM!", + "=EDATE(\"\",0)": "#VALUE!", + "=EDATE(\"January 25, 100\",0)": "#VALUE!", // HOUR "=HOUR()": "HOUR requires exactly 1 argument", "=HOUR(-1)": "HOUR only accepts positive argument", From f5d3d59d8c65d9396893ae0156fef21592f6f425 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 11 Jun 2022 14:08:21 +0800 Subject: [PATCH 054/213] ref #65: new formula function EOMONTH --- calc.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++- calc_test.go | 11 +++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/calc.go b/calc.go index fc983e9945..35b9d748eb 100644 --- a/calc.go +++ b/calc.go @@ -435,6 +435,7 @@ type formulaFuncs struct { // EFFECT // EDATE // ENCODEURL +// EOMONTH // ERF // ERF.PRECISE // ERFC @@ -12394,7 +12395,9 @@ func (fn *formulaFuncs) EDATE(argsList *list.List) formulaArg { if month.Number > 11 { y += int(math.Floor(float64(m) / 12)) } - m = int(math.Mod(float64(m), 12)) + if m = m % 12; m < 0 { + m += 12 + } if d > 28 { if days := getDaysInMonth(y, m); d > days { d = days @@ -12404,6 +12407,55 @@ func (fn *formulaFuncs) EDATE(argsList *list.List) formulaArg { return newNumberFormulaArg(result) } +// EOMONTH function returns the last day of the month, that is a specified +// number of months before or after an initial supplied start date. The syntax +// of the function is: +// +// EOMONTH(start_date,months) +// +func (fn *formulaFuncs) EOMONTH(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "EOMONTH requires 2 arguments") + } + date := argsList.Front().Value.(formulaArg) + num := date.ToNumber() + var dateTime time.Time + if num.Type != ArgNumber { + dateString := strings.ToLower(date.Value()) + if !isDateOnlyFmt(dateString) { + if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError { + return err + } + } + y, m, d, _, err := strToDate(dateString) + if err.Type == ArgError { + return err + } + dateTime = time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.Now().Location()) + } else { + if num.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + dateTime = timeFromExcelTime(num.Number, false) + } + months := argsList.Back().Value.(formulaArg).ToNumber() + if months.Type != ArgNumber { + return months + } + y, m := dateTime.Year(), int(dateTime.Month())+int(months.Number)-1 + if m < 0 { + y -= int(math.Ceil(-1 * float64(m) / 12)) + } + if m > 11 { + y += int(math.Floor(float64(m) / 12)) + } + if m = m % 12; m < 0 { + m += 12 + } + result, _ := timeToExcelTime(time.Date(y, time.Month(m+1), getDaysInMonth(y, m+1), 0, 0, 0, 0, time.UTC), false) + return newNumberFormulaArg(result) +} + // HOUR function returns an integer representing the hour component of a // supplied Excel time. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index ab282ca4f8..6c2c64908e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1479,6 +1479,11 @@ func TestCalcCellValue(t *testing.T) { "=EDATE(\"01/01/2021\",-1)": "44166", "=EDATE(\"01/31/2020\",1)": "43890", "=EDATE(\"01/29/2020\",12)": "44225", + "=EDATE(\"6/12/2021\",-14)": "43933", + // EOMONTH + "=EOMONTH(\"01/01/2021\",-1)": "44196", + "=EOMONTH(\"01/29/2020\",12)": "44227", + "=EOMONTH(\"01/12/2021\",-18)": "43677", // HOUR "=HOUR(1)": "0", "=HOUR(43543.5032060185)": "12", @@ -3447,6 +3452,12 @@ func TestCalcCellValue(t *testing.T) { "=EDATE(-1,0)": "#NUM!", "=EDATE(\"\",0)": "#VALUE!", "=EDATE(\"January 25, 100\",0)": "#VALUE!", + // EOMONTH + "=EOMONTH()": "EOMONTH requires 2 arguments", + "=EOMONTH(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=EOMONTH(-1,0)": "#NUM!", + "=EOMONTH(\"\",0)": "#VALUE!", + "=EOMONTH(\"January 25, 100\",0)": "#VALUE!", // HOUR "=HOUR()": "HOUR requires exactly 1 argument", "=HOUR(-1)": "HOUR only accepts positive argument", From 6bcf5e4ede160af2ad04f5e69636211a5ced132d Mon Sep 17 00:00:00 2001 From: Eng Zer Jun Date: Sun, 12 Jun 2022 00:19:12 +0800 Subject: [PATCH 055/213] refactor: replace strings.Replace with strings.ReplaceAll (#1250) strings.ReplaceAll(s, old, new) is a wrapper function for strings.Replace(s, old, new, -1). But strings.ReplaceAll is more readable and removes the hardcoded -1. Signed-off-by: Eng Zer Jun --- calc.go | 10 +++++----- chart.go | 2 +- comment.go | 2 +- drawing.go | 2 +- lib.go | 4 ++-- picture.go | 12 ++++++------ pivotTable.go | 4 ++-- shape.go | 6 +++--- sheet.go | 4 ++-- stream.go | 2 +- styles.go | 4 ++-- table.go | 2 +- 12 files changed, 27 insertions(+), 27 deletions(-) diff --git a/calc.go b/calc.go index 35b9d748eb..e9a676d38c 100644 --- a/calc.go +++ b/calc.go @@ -1360,7 +1360,7 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta // parseReference parse reference and extract values by given reference // characters and default sheet name. func (f *File) parseReference(sheet, reference string) (arg formulaArg, err error) { - reference = strings.Replace(reference, "$", "", -1) + reference = strings.ReplaceAll(reference, "$", "") refs, cellRanges, cellRefs := list.New(), list.New(), list.New() for _, ref := range strings.Split(reference, ":") { tokens := strings.Split(ref, "!") @@ -2065,13 +2065,13 @@ func cmplx2str(num complex128, suffix string) string { c = strings.TrimSuffix(c, "+0i") c = strings.TrimSuffix(c, "-0i") c = strings.NewReplacer("+1i", "+i", "-1i", "-i").Replace(c) - c = strings.Replace(c, "i", suffix, -1) + c = strings.ReplaceAll(c, "i", suffix) return c } // str2cmplx convert complex number string characters. func str2cmplx(c string) string { - c = strings.Replace(c, "j", "i", -1) + c = strings.ReplaceAll(c, "j", "i") if c == "i" { c = "1i" } @@ -13489,7 +13489,7 @@ func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg { text, oldText := argsList.Front().Value.(formulaArg), argsList.Front().Next().Value.(formulaArg) newText, instanceNum := argsList.Front().Next().Next().Value.(formulaArg), 0 if argsList.Len() == 3 { - return newStringFormulaArg(strings.Replace(text.Value(), oldText.Value(), newText.Value(), -1)) + return newStringFormulaArg(strings.ReplaceAll(text.Value(), oldText.Value(), newText.Value())) } instanceNumArg := argsList.Back().Value.(formulaArg).ToNumber() if instanceNumArg.Type != ArgNumber { @@ -14804,7 +14804,7 @@ func (fn *formulaFuncs) ENCODEURL(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "ENCODEURL requires 1 argument") } token := argsList.Front().Value.(formulaArg).Value() - return newStringFormulaArg(strings.Replace(url.QueryEscape(token), "+", "%20", -1)) + return newStringFormulaArg(strings.ReplaceAll(url.QueryEscape(token), "+", "%20")) } // Financial Functions diff --git a/chart.go b/chart.go index 7b7162ba96..7dcbe19bec 100644 --- a/chart.go +++ b/chart.go @@ -1001,7 +1001,7 @@ func (f *File) DeleteChart(sheet, cell string) (err error) { if ws.Drawing == nil { return } - drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1) + drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl") return f.deleteDrawing(col, row, drawingXML, "Chart") } diff --git a/comment.go b/comment.go index a7c1415ba0..0e3945dfa2 100644 --- a/comment.go +++ b/comment.go @@ -112,7 +112,7 @@ func (f *File) AddComment(sheet, cell, format string) error { // The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml. sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID) commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) - drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1) + drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") } else { // Add first comment for given sheet. sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" diff --git a/drawing.go b/drawing.go index d0e9135ba2..7de0fb9136 100644 --- a/drawing.go +++ b/drawing.go @@ -30,7 +30,7 @@ func (f *File) prepareDrawing(ws *xlsxWorksheet, drawingID int, sheet, drawingXM // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) - drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) + drawingXML = strings.ReplaceAll(sheetRelationshipsDrawingXML, "..", "xl") } else { // Add first picture for given sheet. sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" diff --git a/lib.go b/lib.go index 723b976f7a..f285a40dbc 100644 --- a/lib.go +++ b/lib.go @@ -43,7 +43,7 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { if unzipSize > f.options.UnzipSizeLimit { return fileList, worksheets, newUnzipSizeLimitError(f.options.UnzipSizeLimit) } - fileName := strings.Replace(v.Name, "\\", "/", -1) + fileName := strings.ReplaceAll(v.Name, "\\", "/") if partName, ok := docPart[strings.ToLower(fileName)]; ok { fileName = partName } @@ -284,7 +284,7 @@ func CoordinatesToCellName(col, row int, abs ...bool) (string, error) { // areaRefToCoordinates provides a function to convert area reference to a // pair of coordinates. func areaRefToCoordinates(ref string) ([]int, error) { - rng := strings.Split(strings.Replace(ref, "$", "", -1), ":") + rng := strings.Split(strings.ReplaceAll(ref, "$", ""), ":") if len(rng) < 2 { return nil, ErrParameterInvalid } diff --git a/picture.go b/picture.go index 30a66d36f3..44f1f3b3e8 100644 --- a/picture.go +++ b/picture.go @@ -508,12 +508,12 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { return "", nil, err } target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) - drawingXML := strings.Replace(target, "..", "xl", -1) + drawingXML := strings.ReplaceAll(target, "..", "xl") if _, ok := f.Pkg.Load(drawingXML); !ok { return "", nil, err } - drawingRelationships := strings.Replace( - strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1) + drawingRelationships := strings.ReplaceAll( + strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") return f.getPicture(row, col, drawingXML, drawingRelationships) } @@ -535,7 +535,7 @@ func (f *File) DeletePicture(sheet, cell string) (err error) { if ws.Drawing == nil { return } - drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1) + drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl") return f.deleteDrawing(col, row, drawingXML, "Pic") } @@ -573,7 +573,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed) if _, ok = supportedImageTypes[filepath.Ext(drawRel.Target)]; ok { ret = filepath.Base(drawRel.Target) - if buffer, _ := f.Pkg.Load(strings.Replace(drawRel.Target, "..", "xl", -1)); buffer != nil { + if buffer, _ := f.Pkg.Load(strings.ReplaceAll(drawRel.Target, "..", "xl")); buffer != nil { buf = buffer.([]byte) } return @@ -602,7 +602,7 @@ func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsD anchor.Pic.BlipFill.Blip.Embed); drawRel != nil { if _, ok = supportedImageTypes[filepath.Ext(drawRel.Target)]; ok { ret = filepath.Base(drawRel.Target) - if buffer, _ := f.Pkg.Load(strings.Replace(drawRel.Target, "..", "xl", -1)); buffer != nil { + if buffer, _ := f.Pkg.Load(strings.ReplaceAll(drawRel.Target, "..", "xl")); buffer != nil { buf = buffer.([]byte) } return diff --git a/pivotTable.go b/pivotTable.go index de671f756c..10c48cef3a 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -141,7 +141,7 @@ func (f *File) AddPivotTable(opt *PivotTableOption) error { pivotCacheID := f.countPivotCache() + 1 sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml" - pivotTableXML := strings.Replace(sheetRelationshipsPivotTableXML, "..", "xl", -1) + pivotTableXML := strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl") pivotCacheXML := "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml" err = f.addPivotCache(pivotCacheXML, opt) if err != nil { @@ -206,7 +206,7 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { if len(rng) != 2 { return "", []int{}, ErrParameterInvalid } - trimRng := strings.Replace(rng[1], "$", "", -1) + trimRng := strings.ReplaceAll(rng[1], "$", "") coordinates, err := areaRefToCoordinates(trimRng) if err != nil { return rng[0], []int{}, err diff --git a/shape.go b/shape.go index 6d86f3800d..ddf9e317c6 100644 --- a/shape.go +++ b/shape.go @@ -297,7 +297,7 @@ func (f *File) AddShape(sheet, cell, format string) error { // The worksheet already has a shape or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) - drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) + drawingXML = strings.ReplaceAll(sheetRelationshipsDrawingXML, "..", "xl") } else { // Add first shape for given sheet. sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" @@ -448,7 +448,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format Lang: "en-US", }, } - srgbClr := strings.Replace(strings.ToUpper(p.Font.Color), "#", "", -1) + srgbClr := strings.ReplaceAll(strings.ToUpper(p.Font.Color), "#", "") if len(srgbClr) == 6 { paragraph.R.RPr.SolidFill = &aSolidFill{ SrgbClr: &attrValString{ @@ -484,7 +484,7 @@ func setShapeRef(color string, i int) *aRef { return &aRef{ Idx: i, SrgbClr: &attrValString{ - Val: stringPtr(strings.Replace(strings.ToUpper(color), "#", "", -1)), + Val: stringPtr(strings.ReplaceAll(strings.ToUpper(color), "#", "")), }, } } diff --git a/sheet.go b/sheet.go index 47a206387f..7b6e5dc275 100644 --- a/sheet.go +++ b/sheet.go @@ -99,9 +99,9 @@ func (f *File) contentTypesWriter() { // and /xl/worksheets/sheet%d.xml func (f *File) getWorksheetPath(relTarget string) (path string) { path = filepath.ToSlash(strings.TrimPrefix( - strings.Replace(filepath.Clean(fmt.Sprintf("%s/%s", filepath.Dir(f.getWorkbookPath()), relTarget)), "\\", "/", -1), "/")) + strings.ReplaceAll(filepath.Clean(fmt.Sprintf("%s/%s", filepath.Dir(f.getWorkbookPath()), relTarget)), "\\", "/"), "/")) if strings.HasPrefix(relTarget, "/") { - path = filepath.ToSlash(strings.TrimPrefix(strings.Replace(filepath.Clean(relTarget), "\\", "/", -1), "/")) + path = filepath.ToSlash(strings.TrimPrefix(strings.ReplaceAll(filepath.Clean(relTarget), "\\", "/"), "/")) } return path } diff --git a/stream.go b/stream.go index e1a12bec00..641340ede9 100644 --- a/stream.go +++ b/stream.go @@ -206,7 +206,7 @@ func (sw *StreamWriter) AddTable(hCell, vCell, format string) error { } sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml" - tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1) + tableXML := strings.ReplaceAll(sheetRelationshipsTableXML, "..", "xl") // Add first table for given sheet. sheetPath := sw.File.sheetMap[trimSheetName(sw.Sheet)] diff --git a/styles.go b/styles.go index f8f4030489..0220e9c976 100644 --- a/styles.go +++ b/styles.go @@ -2123,7 +2123,7 @@ func newNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { if !currency { return setLangNumFmt(styleSheet, style) } - fc = strings.Replace(fc, "0.00", dp, -1) + fc = strings.ReplaceAll(fc, "0.00", dp) if style.NegRed { fc = fc + ";[Red]" + fc } @@ -3018,7 +3018,7 @@ func drawConfFmtExp(p int, ct string, format *formatConditional) *xlsxCfRule { // getPaletteColor provides a function to convert the RBG color by given // string. func getPaletteColor(color string) string { - return "FF" + strings.Replace(strings.ToUpper(color), "#", "", -1) + return "FF" + strings.ReplaceAll(strings.ToUpper(color), "#", "") } // themeReader provides a function to get the pointer to the xl/theme/theme1.xml diff --git a/table.go b/table.go index b01c1cb7e8..413118c31a 100644 --- a/table.go +++ b/table.go @@ -83,7 +83,7 @@ func (f *File) AddTable(sheet, hCell, vCell, format string) error { tableID := f.countTables() + 1 sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml" - tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1) + tableXML := strings.ReplaceAll(sheetRelationshipsTableXML, "..", "xl") // Add first table for given sheet. sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipTable, sheetRelationshipsTableXML, "") From d383f0ae6e253284520d10e574a01b0b904c91d9 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 13 Jun 2022 00:05:52 +0800 Subject: [PATCH 056/213] ref #65: new formula function WORKDAY.INTL --- calc.go | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 66 ++++++++++++++++++ 2 files changed, 258 insertions(+) diff --git a/calc.go b/calc.go index e9a676d38c..a485a61522 100644 --- a/calc.go +++ b/calc.go @@ -724,6 +724,7 @@ type formulaFuncs struct { // WEEKNUM // WEIBULL // WEIBULL.DIST +// WORKDAY.INTL // XIRR // XLOOKUP // XNPV @@ -12552,6 +12553,197 @@ func (fn *formulaFuncs) MONTH(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Month())) } +// genWeekendMask generate weekend mask of a series of seven 0's and 1's which +// represent the seven weekdays, starting from Monday. +func genWeekendMask(weekend int) []byte { + mask := make([]byte, 7) + if masks, ok := map[int][]int{ + 1: {5, 6}, 2: {6, 0}, 3: {0, 1}, 4: {1, 2}, 5: {2, 3}, 6: {3, 4}, 7: {4, 5}, + 11: {6}, 12: {0}, 13: {1}, 14: {2}, 15: {3}, 16: {4}, 17: {5}, + }[weekend]; ok { + for _, idx := range masks { + mask[idx] = 1 + } + } + return mask +} + +// isWorkday check if the date is workday. +func isWorkday(weekendMask []byte, date float64) bool { + dateTime := timeFromExcelTime(date, false) + weekday := dateTime.Weekday() + if weekday == time.Sunday { + weekday = 7 + } + return weekendMask[weekday-1] == 0 +} + +// prepareWorkday returns weekend mask and workdays pre week by given days +// counted as weekend. +func prepareWorkday(weekend formulaArg) ([]byte, int) { + weekendArg := weekend.ToNumber() + if weekendArg.Type != ArgNumber { + return nil, 0 + } + var weekendMask []byte + var workdaysPerWeek int + if len(weekend.Value()) == 7 { + // possible string values for the weekend argument + for _, mask := range weekend.Value() { + if mask != '0' && mask != '1' { + return nil, 0 + } + weekendMask = append(weekendMask, byte(mask)-48) + } + } else { + weekendMask = genWeekendMask(int(weekendArg.Number)) + } + for _, mask := range weekendMask { + if mask == 0 { + workdaysPerWeek++ + } + } + return weekendMask, workdaysPerWeek +} + +// toExcelDateArg function converts a text representation of a time, into an +// Excel date time number formula argument. +func toExcelDateArg(arg formulaArg) formulaArg { + num := arg.ToNumber() + if num.Type != ArgNumber { + dateString := strings.ToLower(arg.Value()) + if !isDateOnlyFmt(dateString) { + if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError { + return err + } + } + y, m, d, _, err := strToDate(dateString) + if err.Type == ArgError { + return err + } + num.Number, _ = timeToExcelTime(time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC), false) + return newNumberFormulaArg(num.Number) + } + if arg.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return num +} + +// prepareHolidays function converts array type formula arguments to into an +// Excel date time number formula arguments list. +func prepareHolidays(args formulaArg) []int { + var holidays []int + for _, arg := range args.ToList() { + num := toExcelDateArg(arg) + if num.Type != ArgNumber { + continue + } + holidays = append(holidays, int(math.Ceil(num.Number))) + } + return holidays +} + +// workdayIntl is an implementation of the formula function WORKDAY.INTL. +func workdayIntl(endDate, sign int, holidays []int, weekendMask []byte, startDate float64) int { + for i := 0; i < len(holidays); i++ { + holiday := holidays[i] + if sign > 0 { + if holiday > endDate { + break + } + } else { + if holiday < endDate { + break + } + } + if sign > 0 { + if holiday > int(math.Ceil(startDate)) { + if isWorkday(weekendMask, float64(holiday)) { + endDate += sign + for !isWorkday(weekendMask, float64(endDate)) { + endDate += sign + } + } + } + } else { + if holiday < int(math.Ceil(startDate)) { + if isWorkday(weekendMask, float64(holiday)) { + endDate += sign + for !isWorkday(weekendMask, float64(endDate)) { + endDate += sign + } + } + } + } + } + return endDate +} + +// WORKDAYdotINTL function returns a date that is a supplied number of working +// days (excluding weekends and holidays) ahead of a given start date. The +// function allows the user to specify which days of the week are counted as +// weekends. The syntax of the function is: +// +// WORKDAY.INTL(start_date,days,[weekend],[holidays]) +// +func (fn *formulaFuncs) WORKDAYdotINTL(argsList *list.List) formulaArg { + if argsList.Len() < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY.INTL requires at least 2 arguments") + } + if argsList.Len() > 4 { + return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY.INTL requires at most 4 arguments") + } + startDate := toExcelDateArg(argsList.Front().Value.(formulaArg)) + if startDate.Type != ArgNumber { + return startDate + } + days := argsList.Front().Next().Value.(formulaArg).ToNumber() + if days.Type != ArgNumber { + return days + } + weekend := newNumberFormulaArg(1) + if argsList.Len() > 2 { + weekend = argsList.Front().Next().Next().Value.(formulaArg) + } + var holidays []int + if argsList.Len() == 4 { + holidays = prepareHolidays(argsList.Back().Value.(formulaArg)) + sort.Ints(holidays) + } + if days.Number == 0 { + return newNumberFormulaArg(math.Ceil(startDate.Number)) + } + weekendMask, workdaysPerWeek := prepareWorkday(weekend) + if workdaysPerWeek == 0 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + sign := 1 + if days.Number < 0 { + sign = -1 + } + offset := int(days.Number) / workdaysPerWeek + daysMod := int(days.Number) % workdaysPerWeek + endDate := int(math.Ceil(startDate.Number)) + offset*7 + if daysMod == 0 { + for !isWorkday(weekendMask, float64(endDate)) { + endDate -= sign + } + } else { + for daysMod != 0 { + endDate += sign + if isWorkday(weekendMask, float64(endDate)) { + if daysMod < 0 { + daysMod++ + continue + } + daysMod-- + } + } + } + return newNumberFormulaArg(float64(workdayIntl(endDate, sign, holidays, weekendMask, startDate.Number))) +} + // YEAR function returns an integer representing the year of a supplied date. // The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 6c2c64908e..714211df8e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -5379,6 +5379,72 @@ func TestCalcTTEST(t *testing.T) { } } +func TestCalcWORKDAYdotINTL(t *testing.T) { + cellData := [][]interface{}{ + {"05/01/2019", 43586}, + {"09/13/2019", 43721}, + {"10/01/2019", 43739}, + {"12/25/2019", 43824}, + {"01/01/2020", 43831}, + {"01/01/2020", 43831}, + {"01/24/2020", 43854}, + {"04/04/2020", 43925}, + {"05/01/2020", 43952}, + {"06/25/2020", 44007}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=WORKDAY.INTL(\"12/01/2015\",0)": "42339", + "=WORKDAY.INTL(\"12/01/2015\",25)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",-25)": "42304", + "=WORKDAY.INTL(\"12/01/2015\",25,1)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",25,2)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",25,3)": "42372", + "=WORKDAY.INTL(\"12/01/2015\",25,4)": "42373", + "=WORKDAY.INTL(\"12/01/2015\",25,5)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",25,6)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",25,7)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",25,11)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,12)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,13)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,14)": "42369", + "=WORKDAY.INTL(\"12/01/2015\",25,15)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,16)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,17)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,\"0001100\")": "42374", + "=WORKDAY.INTL(\"01/01/2020\",-123,4)": "43659", + "=WORKDAY.INTL(\"01/01/2020\",123,4,44010)": "44002", + "=WORKDAY.INTL(\"01/01/2020\",-123,4,43640)": "43659", + "=WORKDAY.INTL(\"01/01/2020\",-123,4,43660)": "43658", + "=WORKDAY.INTL(\"01/01/2020\",-123,7,43660)": "43657", + "=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12)": "44008", + "=WORKDAY.INTL(\"01/01/2020\",123,4,B1:B12)": "44008", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError := map[string]string{ + "=WORKDAY.INTL()": "WORKDAY.INTL requires at least 2 arguments", + "=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12,\"\")": "WORKDAY.INTL requires at most 4 arguments", + "=WORKDAY.INTL(\"01/01/2020\",\"\",4,B1:B12)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=WORKDAY.INTL(\"\",123,4,B1:B12)": "#VALUE!", + "=WORKDAY.INTL(\"01/01/2020\",123,\"\",B1:B12)": "#VALUE!", + "=WORKDAY.INTL(\"01/01/2020\",123,\"000000x\")": "#VALUE!", + "=WORKDAY.INTL(\"01/01/2020\",123,\"0000002\")": "#VALUE!", + "=WORKDAY.INTL(\"January 25, 100\",123)": "#VALUE!", + "=WORKDAY.INTL(-1,123)": "#NUM!", + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.EqualError(t, err, expected, formula) + assert.Equal(t, "", result, formula) + } +} + func TestCalcZTEST(t *testing.T) { f := NewFile() assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{4, 5, 2, 5, 8, 9, 3, 2, 3, 8, 9, 5})) From d490a0f86f02f7f2eeabd8a0f38cbaa82a669187 Mon Sep 17 00:00:00 2001 From: jialei <31276367+MichealJl@users.noreply.github.com> Date: Mon, 13 Jun 2022 23:38:59 +0800 Subject: [PATCH 057/213] RichTextRun support set superscript and subscript by vertAlign attribute (#1252) check vertical align enumeration, update set rich text docs and test --- cell.go | 28 +++++++++++++++++++++++----- cell_test.go | 23 +++++++++++++++++++---- xmlStyles.go | 1 + 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/cell.go b/cell.go index a302017795..6d4c62b92d 100644 --- a/cell.go +++ b/cell.go @@ -848,7 +848,10 @@ func newRpr(fnt *Font) *xlsxRPr { if fnt.Family != "" { rpr.RFont = &attrValString{Val: &fnt.Family} } - if fnt.Size > 0.0 { + if inStrSlice([]string{"baseline", "superscript", "subscript"}, fnt.VertAlign, true) != -1 { + rpr.VertAlign = &attrValString{Val: &fnt.VertAlign} + } + if fnt.Size > 0 { rpr.Sz = &attrValFloat{Val: &fnt.Size} } if fnt.Color != "" { @@ -895,7 +898,7 @@ func newRpr(fnt *Font) *xlsxRPr { // }, // }, // { -// Text: " italic", +// Text: "italic ", // Font: &excelize.Font{ // Bold: true, // Color: "e83723", @@ -926,19 +929,34 @@ func newRpr(fnt *Font) *xlsxRPr { // }, // }, // { +// Text: " superscript", +// Font: &excelize.Font{ +// Color: "dbc21f", +// VertAlign: "superscript", +// }, +// }, +// { // Text: " and ", // Font: &excelize.Font{ -// Size: 14, -// Color: "ad23e8", +// Size: 14, +// Color: "ad23e8", +// VertAlign: "baseline", // }, // }, // { -// Text: "underline.", +// Text: "underline", // Font: &excelize.Font{ // Color: "23e833", // Underline: "single", // }, // }, +// { +// Text: " subscript.", +// Font: &excelize.Font{ +// Color: "017505", +// VertAlign: "subscript", +// }, +// }, // }); err != nil { // fmt.Println(err) // return diff --git a/cell_test.go b/cell_test.go index da251cdf13..fb1e8ef585 100644 --- a/cell_test.go +++ b/cell_test.go @@ -590,7 +590,7 @@ func TestSetCellRichText(t *testing.T) { }, }, { - Text: "text with color and font-family,", + Text: "text with color and font-family, ", Font: &Font{ Bold: true, Color: "2354e8", @@ -611,20 +611,35 @@ func TestSetCellRichText(t *testing.T) { Strike: true, }, }, + { + Text: " superscript", + Font: &Font{ + Color: "dbc21f", + VertAlign: "superscript", + }, + }, { Text: " and ", Font: &Font{ - Size: 14, - Color: "ad23e8", + Size: 14, + Color: "ad23e8", + VertAlign: "BASELINE", }, }, { - Text: "underline.", + Text: "underline", Font: &Font{ Color: "23e833", Underline: "single", }, }, + { + Text: " subscript.", + Font: &Font{ + Color: "017505", + VertAlign: "subscript", + }, + }, } assert.NoError(t, f.SetCellRichText("Sheet1", "A1", richTextRun)) assert.NoError(t, f.SetCellRichText("Sheet1", "A2", richTextRun)) diff --git a/xmlStyles.go b/xmlStyles.go index 71fe9a66f9..0000d45a52 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -341,6 +341,7 @@ type Font struct { Size float64 `json:"size"` Strike bool `json:"strike"` Color string `json:"color"` + VertAlign string `json:"vertAlign"` } // Fill directly maps the fill settings of the cells. From 7f570c74f8623aec6e8f89ff3701f28c3a256ffe Mon Sep 17 00:00:00 2001 From: ww1516123 Date: Tue, 14 Jun 2022 15:04:43 +0800 Subject: [PATCH 058/213] Fix the problem of multi arguments calculation (#1253) --- calc.go | 7 +++++++ calc_test.go | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/calc.go b/calc.go index a485a61522..7e89502143 100644 --- a/calc.go +++ b/calc.go @@ -899,6 +899,13 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, if result.Type == ArgUnknown { return newEmptyFormulaArg(), errors.New(formulaErrorVALUE) } + // when thisToken is Range and nextToken is Argument and opfdStack not Empty, should push value to opfdStack and continue. + if nextToken.TType == efp.TokenTypeArgument { + if !opfdStack.Empty() { + opfdStack.Push(result) + continue + } + } argsStack.Peek().(*list.List).PushBack(result) continue } diff --git a/calc_test.go b/calc_test.go index 714211df8e..d5c263e497 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1399,9 +1399,10 @@ func TestCalcCellValue(t *testing.T) { // FALSE "=FALSE()": "FALSE", // IFERROR - "=IFERROR(1/2,0)": "0.5", - "=IFERROR(ISERROR(),0)": "0", - "=IFERROR(1/0,0)": "0", + "=IFERROR(1/2,0)": "0.5", + "=IFERROR(ISERROR(),0)": "0", + "=IFERROR(1/0,0)": "0", + "=IFERROR(B2/MROUND(A2,1),0)": "2.5", // IFNA "=IFNA(1,\"not found\")": "1", "=IFNA(NA(),\"not found\")": "not found", From 5beeeef570e0d5a09de546dfe369a0f3753cf709 Mon Sep 17 00:00:00 2001 From: "z.hua" <276675879@qq.com> Date: Wed, 15 Jun 2022 17:28:59 +0800 Subject: [PATCH 059/213] This closes #1254, `DeleteDataValidation` support delete all data validations in the worksheet --- datavalidation.go | 11 ++++++++--- datavalidation_test.go | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 4df2c505ae..1b06b6a7e2 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -262,8 +262,9 @@ func (f *File) AddDataValidation(sheet string, dv *DataValidation) error { } // DeleteDataValidation delete data validation by given worksheet name and -// reference sequence. -func (f *File) DeleteDataValidation(sheet, sqref string) error { +// reference sequence. All data validations in the worksheet will be deleted +// if not specify reference sequence parameter. +func (f *File) DeleteDataValidation(sheet string, sqref ...string) error { ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -271,7 +272,11 @@ func (f *File) DeleteDataValidation(sheet, sqref string) error { if ws.DataValidations == nil { return nil } - delCells, err := f.flatSqref(sqref) + if sqref == nil { + ws.DataValidations = nil + return nil + } + delCells, err := f.flatSqref(sqref[0]) if err != nil { return err } diff --git a/datavalidation_test.go b/datavalidation_test.go index 9ef11dcd38..80cbf59547 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -171,4 +171,8 @@ func TestDeleteDataValidation(t *testing.T) { // Test delete data validation on no exists worksheet. assert.EqualError(t, f.DeleteDataValidation("SheetN", "A1:B2"), "sheet SheetN is not exist") + + // Test delete all data validations in the worksheet + assert.NoError(t, f.DeleteDataValidation("Sheet1")) + assert.Nil(t, ws.(*xlsxWorksheet).DataValidations) } From b69da7606395bb2b05c53512663a13cce80f87d7 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 16 Jun 2022 00:01:32 +0800 Subject: [PATCH 060/213] ref #65, new formula functions: NETWORKDAYS, NETWORKDAYS.INTL, and WORKDAY --- calc.go | 126 ++++++++++++++++++++++++++++++++++++++--- calc_test.go | 111 ++++++++++++++++++++++++------------ datavalidation_test.go | 2 +- rows.go | 14 +++-- 4 files changed, 203 insertions(+), 50 deletions(-) diff --git a/calc.go b/calc.go index 7e89502143..6da0f6aaad 100644 --- a/calc.go +++ b/calc.go @@ -582,6 +582,8 @@ type formulaFuncs struct { // NA // NEGBINOM.DIST // NEGBINOMDIST +// NETWORKDAYS +// NETWORKDAYS.INTL // NOMINAL // NORM.DIST // NORMDIST @@ -724,6 +726,7 @@ type formulaFuncs struct { // WEEKNUM // WEIBULL // WEIBULL.DIST +// WORKDAY // WORKDAY.INTL // XIRR // XLOOKUP @@ -899,12 +902,11 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, if result.Type == ArgUnknown { return newEmptyFormulaArg(), errors.New(formulaErrorVALUE) } - // when thisToken is Range and nextToken is Argument and opfdStack not Empty, should push value to opfdStack and continue. - if nextToken.TType == efp.TokenTypeArgument { - if !opfdStack.Empty() { - opfdStack.Push(result) - continue - } + // when current token is range, next token is argument and opfdStack not empty, + // should push value to opfdStack and continue + if nextToken.TType == efp.TokenTypeArgument && !opfdStack.Empty() { + opfdStack.Push(result) + continue } argsStack.Peek().(*list.List).PushBack(result) continue @@ -12563,16 +12565,17 @@ func (fn *formulaFuncs) MONTH(argsList *list.List) formulaArg { // genWeekendMask generate weekend mask of a series of seven 0's and 1's which // represent the seven weekdays, starting from Monday. func genWeekendMask(weekend int) []byte { - mask := make([]byte, 7) if masks, ok := map[int][]int{ 1: {5, 6}, 2: {6, 0}, 3: {0, 1}, 4: {1, 2}, 5: {2, 3}, 6: {3, 4}, 7: {4, 5}, 11: {6}, 12: {0}, 13: {1}, 14: {2}, 15: {3}, 16: {4}, 17: {5}, }[weekend]; ok { + mask := make([]byte, 7) for _, idx := range masks { mask[idx] = 1 } + return mask } - return mask + return nil } // isWorkday check if the date is workday. @@ -12687,6 +12690,113 @@ func workdayIntl(endDate, sign int, holidays []int, weekendMask []byte, startDat return endDate } +// NETWORKDAYS function calculates the number of work days between two supplied +// dates (including the start and end date). The calculation includes all +// weekdays (Mon - Fri), excluding a supplied list of holidays. The syntax of +// the function is: +// +// NETWORKDAYS(start_date,end_date,[holidays]) +// +func (fn *formulaFuncs) NETWORKDAYS(argsList *list.List) formulaArg { + if argsList.Len() < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS requires at least 2 arguments") + } + if argsList.Len() > 3 { + return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS requires at most 3 arguments") + } + args := list.New() + args.PushBack(argsList.Front().Value.(formulaArg)) + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + args.PushBack(newNumberFormulaArg(1)) + if argsList.Len() == 3 { + args.PushBack(argsList.Back().Value.(formulaArg)) + } + return fn.NETWORKDAYSdotINTL(args) +} + +// NETWORKDAYSdotINTL function calculates the number of whole work days between +// two supplied dates, excluding weekends and holidays. The function allows +// the user to specify which days are counted as weekends and holidays. The +// syntax of the function is: +// +// NETWORKDAYS.INTL(start_date,end_date,[weekend],[holidays]) +// +func (fn *formulaFuncs) NETWORKDAYSdotINTL(argsList *list.List) formulaArg { + if argsList.Len() < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS.INTL requires at least 2 arguments") + } + if argsList.Len() > 4 { + return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS.INTL requires at most 4 arguments") + } + startDate := toExcelDateArg(argsList.Front().Value.(formulaArg)) + if startDate.Type != ArgNumber { + return startDate + } + endDate := toExcelDateArg(argsList.Front().Next().Value.(formulaArg)) + if endDate.Type != ArgNumber { + return endDate + } + weekend := newNumberFormulaArg(1) + if argsList.Len() > 2 { + weekend = argsList.Front().Next().Next().Value.(formulaArg) + } + var holidays []int + if argsList.Len() == 4 { + holidays = prepareHolidays(argsList.Back().Value.(formulaArg)) + sort.Ints(holidays) + } + weekendMask, workdaysPerWeek := prepareWorkday(weekend) + if workdaysPerWeek == 0 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + sign := 1 + if startDate.Number > endDate.Number { + sign = -1 + temp := startDate.Number + startDate.Number = endDate.Number + endDate.Number = temp + } + offset := endDate.Number - startDate.Number + count := int(math.Floor(offset/7) * float64(workdaysPerWeek)) + daysMod := int(offset) % 7 + for daysMod >= 0 { + if isWorkday(weekendMask, endDate.Number-float64(daysMod)) { + count++ + } + daysMod-- + } + for i := 0; i < len(holidays); i++ { + holiday := float64(holidays[i]) + if isWorkday(weekendMask, holiday) && holiday >= startDate.Number && holiday <= endDate.Number { + count-- + } + } + return newNumberFormulaArg(float64(sign * count)) +} + +// WORKDAY function returns a date that is a supplied number of working days +// (excluding weekends and holidays) ahead of a given start date. The syntax +// of the function is: +// +// WORKDAY(start_date,days,[holidays]) +// +func (fn *formulaFuncs) WORKDAY(argsList *list.List) formulaArg { + if argsList.Len() < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY requires at least 2 arguments") + } + if argsList.Len() > 3 { + return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY requires at most 3 arguments") + } + args := list.New() + args.PushBack(argsList.Front().Value.(formulaArg)) + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + args.PushBack(newNumberFormulaArg(1)) + if argsList.Len() == 3 { + args.PushBack(argsList.Back().Value.(formulaArg)) + } + return fn.WORKDAYdotINTL(args) +} + // WORKDAYdotINTL function returns a date that is a supplied number of working // days (excluding weekends and holidays) ahead of a given start date. The // function allows the user to specify which days of the week are counted as diff --git a/calc_test.go b/calc_test.go index d5c263e497..c7333c56d3 100644 --- a/calc_test.go +++ b/calc_test.go @@ -5380,7 +5380,7 @@ func TestCalcTTEST(t *testing.T) { } } -func TestCalcWORKDAYdotINTL(t *testing.T) { +func TestCalcNETWORKDAYSandWORKDAY(t *testing.T) { cellData := [][]interface{}{ {"05/01/2019", 43586}, {"09/13/2019", 43721}, @@ -5395,31 +5395,53 @@ func TestCalcWORKDAYdotINTL(t *testing.T) { } f := prepareCalcData(cellData) formulaList := map[string]string{ - "=WORKDAY.INTL(\"12/01/2015\",0)": "42339", - "=WORKDAY.INTL(\"12/01/2015\",25)": "42374", - "=WORKDAY.INTL(\"12/01/2015\",-25)": "42304", - "=WORKDAY.INTL(\"12/01/2015\",25,1)": "42374", - "=WORKDAY.INTL(\"12/01/2015\",25,2)": "42374", - "=WORKDAY.INTL(\"12/01/2015\",25,3)": "42372", - "=WORKDAY.INTL(\"12/01/2015\",25,4)": "42373", - "=WORKDAY.INTL(\"12/01/2015\",25,5)": "42374", - "=WORKDAY.INTL(\"12/01/2015\",25,6)": "42374", - "=WORKDAY.INTL(\"12/01/2015\",25,7)": "42374", - "=WORKDAY.INTL(\"12/01/2015\",25,11)": "42368", - "=WORKDAY.INTL(\"12/01/2015\",25,12)": "42368", - "=WORKDAY.INTL(\"12/01/2015\",25,13)": "42368", - "=WORKDAY.INTL(\"12/01/2015\",25,14)": "42369", - "=WORKDAY.INTL(\"12/01/2015\",25,15)": "42368", - "=WORKDAY.INTL(\"12/01/2015\",25,16)": "42368", - "=WORKDAY.INTL(\"12/01/2015\",25,17)": "42368", - "=WORKDAY.INTL(\"12/01/2015\",25,\"0001100\")": "42374", - "=WORKDAY.INTL(\"01/01/2020\",-123,4)": "43659", - "=WORKDAY.INTL(\"01/01/2020\",123,4,44010)": "44002", - "=WORKDAY.INTL(\"01/01/2020\",-123,4,43640)": "43659", - "=WORKDAY.INTL(\"01/01/2020\",-123,4,43660)": "43658", - "=WORKDAY.INTL(\"01/01/2020\",-123,7,43660)": "43657", - "=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12)": "44008", - "=WORKDAY.INTL(\"01/01/2020\",123,4,B1:B12)": "44008", + "=NETWORKDAYS(\"01/01/2020\",\"09/12/2020\")": "183", + "=NETWORKDAYS(\"01/01/2020\",\"09/12/2020\",2)": "183", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\")": "183", + "=NETWORKDAYS.INTL(\"09/12/2020\",\"01/01/2020\")": "-183", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1)": "183", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",2)": "184", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",3)": "184", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",4)": "183", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",5)": "182", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",6)": "182", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",7)": "182", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",11)": "220", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",12)": "220", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",13)": "220", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",14)": "219", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",15)": "219", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",16)": "219", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",17)": "219", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,A1:A12)": "178", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,B1:B12)": "178", + "=WORKDAY(\"12/01/2015\",25)": "42374", + "=WORKDAY(\"01/01/2020\",123,B1:B12)": "44006", + "=WORKDAY.INTL(\"12/01/2015\",0)": "42339", + "=WORKDAY.INTL(\"12/01/2015\",25)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",-25)": "42304", + "=WORKDAY.INTL(\"12/01/2015\",25,1)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",25,2)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",25,3)": "42372", + "=WORKDAY.INTL(\"12/01/2015\",25,4)": "42373", + "=WORKDAY.INTL(\"12/01/2015\",25,5)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",25,6)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",25,7)": "42374", + "=WORKDAY.INTL(\"12/01/2015\",25,11)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,12)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,13)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,14)": "42369", + "=WORKDAY.INTL(\"12/01/2015\",25,15)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,16)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,17)": "42368", + "=WORKDAY.INTL(\"12/01/2015\",25,\"0001100\")": "42374", + "=WORKDAY.INTL(\"01/01/2020\",-123,4)": "43659", + "=WORKDAY.INTL(\"01/01/2020\",123,4,44010)": "44002", + "=WORKDAY.INTL(\"01/01/2020\",-123,4,43640)": "43659", + "=WORKDAY.INTL(\"01/01/2020\",-123,4,43660)": "43658", + "=WORKDAY.INTL(\"01/01/2020\",-123,7,43660)": "43657", + "=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12)": "44008", + "=WORKDAY.INTL(\"01/01/2020\",123,4,B1:B12)": "44008", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) @@ -5428,15 +5450,34 @@ func TestCalcWORKDAYdotINTL(t *testing.T) { assert.Equal(t, expected, result, formula) } calcError := map[string]string{ - "=WORKDAY.INTL()": "WORKDAY.INTL requires at least 2 arguments", - "=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12,\"\")": "WORKDAY.INTL requires at most 4 arguments", - "=WORKDAY.INTL(\"01/01/2020\",\"\",4,B1:B12)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=WORKDAY.INTL(\"\",123,4,B1:B12)": "#VALUE!", - "=WORKDAY.INTL(\"01/01/2020\",123,\"\",B1:B12)": "#VALUE!", - "=WORKDAY.INTL(\"01/01/2020\",123,\"000000x\")": "#VALUE!", - "=WORKDAY.INTL(\"01/01/2020\",123,\"0000002\")": "#VALUE!", - "=WORKDAY.INTL(\"January 25, 100\",123)": "#VALUE!", - "=WORKDAY.INTL(-1,123)": "#NUM!", + "=NETWORKDAYS()": "NETWORKDAYS requires at least 2 arguments", + "=NETWORKDAYS(\"01/01/2020\",\"09/12/2020\",2,\"\")": "NETWORKDAYS requires at most 3 arguments", + "=NETWORKDAYS(\"\",\"09/12/2020\",2)": "#VALUE!", + "=NETWORKDAYS(\"01/01/2020\",\"\",2)": "#VALUE!", + "=NETWORKDAYS.INTL()": "NETWORKDAYS.INTL requires at least 2 arguments", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",4,A1:A12,\"\")": "NETWORKDAYS.INTL requires at most 4 arguments", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"January 25, 100\",4)": "#VALUE!", + "=NETWORKDAYS.INTL(\"\",123,4,B1:B12)": "#VALUE!", + "=NETWORKDAYS.INTL(\"01/01/2020\",123,\"000000x\")": "#VALUE!", + "=NETWORKDAYS.INTL(\"01/01/2020\",123,\"0000002\")": "#VALUE!", + "=NETWORKDAYS.INTL(\"January 25, 100\",123)": "#VALUE!", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",8)": "#VALUE!", + "=NETWORKDAYS.INTL(-1,123)": "#NUM!", + "=WORKDAY()": "WORKDAY requires at least 2 arguments", + "=WORKDAY(\"01/01/2020\",123,A1:A12,\"\")": "WORKDAY requires at most 3 arguments", + "=WORKDAY(\"01/01/2020\",\"\",B1:B12)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=WORKDAY(\"\",123,B1:B12)": "#VALUE!", + "=WORKDAY(\"January 25, 100\",123)": "#VALUE!", + "=WORKDAY(-1,123)": "#NUM!", + "=WORKDAY.INTL()": "WORKDAY.INTL requires at least 2 arguments", + "=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12,\"\")": "WORKDAY.INTL requires at most 4 arguments", + "=WORKDAY.INTL(\"01/01/2020\",\"\",4,B1:B12)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=WORKDAY.INTL(\"\",123,4,B1:B12)": "#VALUE!", + "=WORKDAY.INTL(\"01/01/2020\",123,\"\",B1:B12)": "#VALUE!", + "=WORKDAY.INTL(\"01/01/2020\",123,\"000000x\")": "#VALUE!", + "=WORKDAY.INTL(\"01/01/2020\",123,\"0000002\")": "#VALUE!", + "=WORKDAY.INTL(\"January 25, 100\",123)": "#VALUE!", + "=WORKDAY.INTL(-1,123)": "#NUM!", } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) diff --git a/datavalidation_test.go b/datavalidation_test.go index 80cbf59547..d9e060a54e 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -172,7 +172,7 @@ func TestDeleteDataValidation(t *testing.T) { // Test delete data validation on no exists worksheet. assert.EqualError(t, f.DeleteDataValidation("SheetN", "A1:B2"), "sheet SheetN is not exist") - // Test delete all data validations in the worksheet + // Test delete all data validations in the worksheet. assert.NoError(t, f.DeleteDataValidation("Sheet1")) assert.Nil(t, ws.(*xlsxWorksheet).DataValidations) } diff --git a/rows.go b/rows.go index bcb8960de6..f83d425382 100644 --- a/rows.go +++ b/rows.go @@ -28,11 +28,11 @@ import ( // GetRows return all the rows in a sheet by given worksheet name // (case sensitive), returned as a two-dimensional array, where the value of -// the cell is converted to the string type. If the cell format can be -// applied to the value of the cell, the applied value will be used, -// otherwise the original value will be used. GetRows fetched the rows with -// value or formula cells, the tail continuously empty cell will be skipped. -// For example: +// the cell is converted to the string type. If the cell format can be applied +// to the value of the cell, the applied value will be used, otherwise the +// original value will be used. GetRows fetched the rows with value or formula +// cells, the continually blank cells in the tail of each row will be skipped, +// so the length of each row may be inconsistent. For example: // // rows, err := f.GetRows("Sheet1") // if err != nil { @@ -122,7 +122,9 @@ func (rows *Rows) Close() error { return nil } -// Columns return the current row's column values. +// Columns return the current row's column values. This fetches the worksheet +// data as a stream, returns each cell in a row as is, and will not skip empty +// rows in the tail of the worksheet. func (rows *Rows) Columns(opts ...Options) ([]string, error) { if rows.curRow > rows.seekRow { return nil, nil From 5f4131aece5071cd98ac080b6ace85726d922f19 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 17 Jun 2022 00:03:31 +0800 Subject: [PATCH 061/213] ref #65, new formula function: DAYS360 --- calc.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 15 +++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/calc.go b/calc.go index 6da0f6aaad..086c288eb3 100644 --- a/calc.go +++ b/calc.go @@ -419,6 +419,7 @@ type formulaFuncs struct { // DATEVALUE // DAY // DAYS +// DAYS360 // DB // DDB // DEC2BIN @@ -12330,6 +12331,57 @@ func (fn *formulaFuncs) DAYS(argsList *list.List) formulaArg { return newNumberFormulaArg(end.Number - start.Number) } +// DAYS360 function returns the number of days between 2 dates, based on a +// 360-day year (12 x 30 months). The syntax of the function is: +// +// DAYS360(start_date,end_date,[method]) +// +func (fn *formulaFuncs) DAYS360(argsList *list.List) formulaArg { + if argsList.Len() < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "DAYS360 requires at least 2 arguments") + } + if argsList.Len() > 3 { + return newErrorFormulaArg(formulaErrorVALUE, "DAYS360 requires at most 3 arguments") + } + startDate := toExcelDateArg(argsList.Front().Value.(formulaArg)) + if startDate.Type != ArgNumber { + return startDate + } + endDate := toExcelDateArg(argsList.Front().Next().Value.(formulaArg)) + if endDate.Type != ArgNumber { + return endDate + } + start, end := timeFromExcelTime(startDate.Number, false), timeFromExcelTime(endDate.Number, false) + sy, sm, sd, ey, em, ed := start.Year(), int(start.Month()), start.Day(), end.Year(), int(end.Month()), end.Day() + method := newBoolFormulaArg(false) + if argsList.Len() > 2 { + if method = argsList.Back().Value.(formulaArg).ToBool(); method.Type != ArgNumber { + return method + } + } + if method.Number == 1 { + if sd == 31 { + sd-- + } + if ed == 31 { + ed-- + } + } else { + if getDaysInMonth(sy, sm) == sd { + sd = 30 + } + if ed > 30 { + if sd < 30 { + em++ + ed = 1 + } else { + ed = 30 + } + } + } + return newNumberFormulaArg(float64(360*(ey-sy) + 30*(em-sm) + (ed - sd))) +} + // ISOWEEKNUM function returns the ISO week number of a supplied date. The // syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index c7333c56d3..c9891a3893 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1476,6 +1476,15 @@ func TestCalcCellValue(t *testing.T) { "=DAYS(2,1)": "1", "=DAYS(INT(2),INT(1))": "1", "=DAYS(\"02/02/2015\",\"01/01/2015\")": "32", + // DAYS360 + "=DAYS360(\"10/10/2020\", \"10/10/2020\")": "0", + "=DAYS360(\"01/30/1999\", \"02/28/1999\")": "28", + "=DAYS360(\"01/31/1999\", \"02/28/1999\")": "28", + "=DAYS360(\"12/12/1999\", \"08/31/1999\")": "-101", + "=DAYS360(\"12/12/1999\", \"11/30/1999\")": "-12", + "=DAYS360(\"12/12/1999\", \"11/30/1999\",TRUE)": "-12", + "=DAYS360(\"01/31/1999\", \"03/31/1999\",TRUE)": "60", + "=DAYS360(\"01/31/1999\", \"03/31/2000\",FALSE)": "420", // EDATE "=EDATE(\"01/01/2021\",-1)": "44166", "=EDATE(\"01/31/2020\",1)": "43890", @@ -3447,6 +3456,12 @@ func TestCalcCellValue(t *testing.T) { "=DAYS(0,\"\")": "#VALUE!", "=DAYS(NA(),0)": "#VALUE!", "=DAYS(0,NA())": "#VALUE!", + // DAYS360 + "=DAYS360(\"12/12/1999\")": "DAYS360 requires at least 2 arguments", + "=DAYS360(\"12/12/1999\", \"11/30/1999\",TRUE,\"\")": "DAYS360 requires at most 3 arguments", + "=DAYS360(\"12/12/1999\", \"11/30/1999\",\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=DAYS360(\"12/12/1999\", \"\")": "#VALUE!", + "=DAYS360(\"\", \"11/30/1999\")": "#VALUE!", // EDATE "=EDATE()": "EDATE requires 2 arguments", "=EDATE(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", From 7819cd7fec50513786a5d47c6f11a59cceba541a Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 20 Jun 2022 22:05:23 +0800 Subject: [PATCH 062/213] ref #65, new formula function: STEYX --- calc.go | 38 ++++++++++++++++++++++++++++++++++++++ calc_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/calc.go b/calc.go index 086c288eb3..6aaf79eff3 100644 --- a/calc.go +++ b/calc.go @@ -674,6 +674,7 @@ type formulaFuncs struct { // STDEVA // STDEVP // STDEVPA +// STEYX // SUBSTITUTE // SUM // SUMIF @@ -10647,6 +10648,43 @@ func (fn *formulaFuncs) STDEVPA(argsList *list.List) formulaArg { return fn.stdevp("STDEVPA", argsList) } +// STEYX function calculates the standard error for the line of best fit, +// through a supplied set of x- and y- values. The syntax of the function is: +// +// STEYX(known_y's,known_x's) +// +func (fn *formulaFuncs) STEYX(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "STEYX requires 2 arguments") + } + array1 := argsList.Back().Value.(formulaArg).ToList() + array2 := argsList.Front().Value.(formulaArg).ToList() + if len(array1) != len(array2) { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + var count, sumX, sumY, squareX, squareY, sigmaXY float64 + for i := 0; i < len(array1); i++ { + num1, num2 := array1[i].ToNumber(), array2[i].ToNumber() + if !(num1.Type == ArgNumber && num2.Type == ArgNumber) { + continue + } + sumX += num1.Number + sumY += num2.Number + squareX += num1.Number * num1.Number + squareY += num2.Number * num2.Number + sigmaXY += num1.Number * num2.Number + count++ + } + if count < 3 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + dx, dy := sumX/count, sumY/count + sigma1 := squareY - 2*dy*sumY + count*dy*dy + sigma2 := sigmaXY - dy*sumX - sumY*dx + count*dy*dx + sigma3 := squareX - 2*dx*sumX + count*dx*dx + return newNumberFormulaArg(math.Sqrt((sigma1 - (sigma2*sigma2)/sigma3) / (count - 2))) +} + // getTDist is an implementation for the beta distribution probability density // function. func getTDist(T, fDF, nType float64) float64 { diff --git a/calc_test.go b/calc_test.go index c9891a3893..92460d7602 100644 --- a/calc_test.go +++ b/calc_test.go @@ -5325,6 +5325,42 @@ func TestCalcSHEETS(t *testing.T) { } } +func TestCalcSTEY(t *testing.T) { + cellData := [][]interface{}{ + {"known_x's", "known_y's"}, + {1, 3}, + {2, 7.9}, + {3, 8}, + {4, 9.2}, + {4.5, 12}, + {5, 10.5}, + {6, 15}, + {7, 15.5}, + {8, 17}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=STEYX(B2:B11,A2:A11)": "1.20118634668221", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError := map[string]string{ + "=STEYX()": "STEYX requires 2 arguments", + "=STEYX(B2:B11,A1:A9)": "#N/A", + "=STEYX(B2,A2)": "#DIV/0!", + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.EqualError(t, err, expected, formula) + assert.Equal(t, "", result, formula) + } +} + func TestCalcTTEST(t *testing.T) { cellData := [][]interface{}{ {4, 8, nil, 1, 1}, From 852f211970b47c79cceedd9de934f9aa7520f131 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 21 Jun 2022 20:08:47 +0800 Subject: [PATCH 063/213] This closes #1257, fix incorrect worksheet header footer fields order --- excelize.go | 8 +++++--- sheet.go | 2 +- xmlContentTypes.go | 2 +- xmlWorksheet.go | 31 +++++++++++++++---------------- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/excelize.go b/excelize.go index aaa4953817..580bc292ed 100644 --- a/excelize.go +++ b/excelize.go @@ -234,9 +234,11 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { ws = worksheet.(*xlsxWorksheet) return } - if strings.HasPrefix(name, "xl/chartsheets") || strings.HasPrefix(name, "xl/macrosheet") { - err = fmt.Errorf("sheet %s is not a worksheet", sheet) - return + for _, sheetType := range []string{"xl/chartsheets", "xl/dialogsheet", "xl/macrosheet"} { + if strings.HasPrefix(name, sheetType) { + err = fmt.Errorf("sheet %s is not a worksheet", sheet) + return + } } ws = new(xlsxWorksheet) if _, ok := f.xmlAttr[name]; !ok { diff --git a/sheet.go b/sheet.go index 7b6e5dc275..45b724f7bb 100644 --- a/sheet.go +++ b/sheet.go @@ -280,7 +280,7 @@ func (f *File) SetActiveSheet(index int) { for idx, name := range f.GetSheetList() { ws, err := f.workSheetReader(name) if err != nil { - // Chartsheet or dialogsheet + // Chartsheet, macrosheet or dialogsheet return } if ws.SheetViews == nil { diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 4b3cd64275..52dd744c0f 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -22,8 +22,8 @@ import ( type xlsxTypes struct { sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/content-types Types"` - Overrides []xlsxOverride `xml:"Override"` Defaults []xlsxDefault `xml:"Default"` + Overrides []xlsxOverride `xml:"Override"` } // xlsxOverride directly maps the override element in the namespace diff --git a/xmlWorksheet.go b/xmlWorksheet.go index eb855c5397..0c0fe92d2b 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -78,18 +78,17 @@ type xlsxDrawing struct { // footers on the first page can differ from those on odd- and even-numbered // pages. In the latter case, the first page is not considered an odd page. type xlsxHeaderFooter struct { - XMLName xml.Name `xml:"headerFooter"` - AlignWithMargins bool `xml:"alignWithMargins,attr,omitempty"` - DifferentFirst bool `xml:"differentFirst,attr,omitempty"` - DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"` - ScaleWithDoc bool `xml:"scaleWithDoc,attr,omitempty"` - OddHeader string `xml:"oddHeader,omitempty"` - OddFooter string `xml:"oddFooter,omitempty"` - EvenHeader string `xml:"evenHeader,omitempty"` - EvenFooter string `xml:"evenFooter,omitempty"` - FirstFooter string `xml:"firstFooter,omitempty"` - FirstHeader string `xml:"firstHeader,omitempty"` - DrawingHF *xlsxDrawingHF `xml:"drawingHF"` + XMLName xml.Name `xml:"headerFooter"` + DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"` + DifferentFirst bool `xml:"differentFirst,attr,omitempty"` + ScaleWithDoc bool `xml:"scaleWithDoc,attr,omitempty"` + AlignWithMargins bool `xml:"alignWithMargins,attr,omitempty"` + OddHeader string `xml:"oddHeader,omitempty"` + OddFooter string `xml:"oddFooter,omitempty"` + EvenHeader string `xml:"evenHeader,omitempty"` + EvenFooter string `xml:"evenFooter,omitempty"` + FirstHeader string `xml:"firstHeader,omitempty"` + FirstFooter string `xml:"firstFooter,omitempty"` } // xlsxDrawingHF (Drawing Reference in Header Footer) specifies the usage of @@ -147,12 +146,12 @@ type xlsxPrintOptions struct { // a sheet or a custom sheet view. type xlsxPageMargins struct { XMLName xml.Name `xml:"pageMargins"` - Bottom float64 `xml:"bottom,attr"` - Footer float64 `xml:"footer,attr"` - Header float64 `xml:"header,attr"` Left float64 `xml:"left,attr"` Right float64 `xml:"right,attr"` Top float64 `xml:"top,attr"` + Bottom float64 `xml:"bottom,attr"` + Header float64 `xml:"header,attr"` + Footer float64 `xml:"footer,attr"` } // xlsxSheetFormatPr directly maps the sheetFormatPr element in the namespace @@ -880,8 +879,8 @@ type FormatHeaderFooter struct { OddFooter string EvenHeader string EvenFooter string - FirstFooter string FirstHeader string + FirstFooter string } // FormatPageMargins directly maps the settings of page margins From 61c71caf4fdd056a45c69d8f3aea2231da2c074a Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 22 Jun 2022 20:17:22 +0800 Subject: [PATCH 064/213] ref #65, new formula function: EUROCONVERT --- calc.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 15 ++++++++++ 2 files changed, 98 insertions(+) diff --git a/calc.go b/calc.go index 6aaf79eff3..c71cd1053c 100644 --- a/calc.go +++ b/calc.go @@ -442,6 +442,7 @@ type formulaFuncs struct { // ERFC // ERFC.PRECISE // ERROR.TYPE +// EUROCONVERT // EVEN // EXACT // EXP @@ -16080,6 +16081,88 @@ func (fn *formulaFuncs) EFFECT(argsList *list.List) formulaArg { return newNumberFormulaArg(math.Pow(1+rate.Number/npery.Number, npery.Number) - 1) } +// EUROCONVERT function convert a number to euro or from euro to a +// participating currency. You can also use it to convert a number from one +// participating currency to another by using the euro as an intermediary +// (triangulation). The syntax of the function is: +// +// EUROCONVERT(number,sourcecurrency,targetcurrency[,fullprecision,triangulationprecision]) +// +func (fn *formulaFuncs) EUROCONVERT(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "EUROCONVERT requires at least 3 arguments") + } + if argsList.Len() > 5 { + return newErrorFormulaArg(formulaErrorVALUE, "EUROCONVERT allows at most 5 arguments") + } + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type != ArgNumber { + return number + } + sourceCurrency := argsList.Front().Next().Value.(formulaArg).Value() + targetCurrency := argsList.Front().Next().Next().Value.(formulaArg).Value() + fullPrec, triangulationPrec := newBoolFormulaArg(false), newNumberFormulaArg(0) + if argsList.Len() >= 4 { + if fullPrec = argsList.Front().Next().Next().Next().Value.(formulaArg).ToBool(); fullPrec.Type != ArgNumber { + return fullPrec + } + } + if argsList.Len() == 5 { + if triangulationPrec = argsList.Back().Value.(formulaArg).ToNumber(); triangulationPrec.Type != ArgNumber { + return triangulationPrec + } + } + convertTable := map[string][]float64{ + "EUR": {1.0, 2}, + "ATS": {13.7603, 2}, + "BEF": {40.3399, 0}, + "DEM": {1.95583, 2}, + "ESP": {166.386, 0}, + "FIM": {5.94573, 2}, + "FRF": {6.55957, 2}, + "IEP": {0.787564, 2}, + "ITL": {1936.27, 0}, + "LUF": {40.3399, 0}, + "NLG": {2.20371, 2}, + "PTE": {200.482, 2}, + "GRD": {340.750, 2}, + "SIT": {239.640, 2}, + "MTL": {0.429300, 2}, + "CYP": {0.585274, 2}, + "SKK": {30.1260, 2}, + "EEK": {15.6466, 2}, + "LVL": {0.702804, 2}, + "LTL": {3.45280, 2}, + } + source, ok := convertTable[sourceCurrency] + if !ok { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + target, ok := convertTable[targetCurrency] + if !ok { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if sourceCurrency == targetCurrency { + return number + } + var res float64 + if sourceCurrency == "EUR" { + res = number.Number * target[0] + } else { + intermediate := number.Number / source[0] + if triangulationPrec.Number != 0 { + ratio := math.Pow(10, triangulationPrec.Number) + intermediate = math.Round(intermediate*ratio) / ratio + } + res = intermediate * target[0] + } + if fullPrec.Number != 1 { + ratio := math.Pow(10, target[1]) + res = math.Round(res*ratio) / ratio + } + return newNumberFormulaArg(res) +} + // FV function calculates the Future Value of an investment with periodic // constant payments and a constant interest rate. The syntax of the function // is: diff --git a/calc_test.go b/calc_test.go index 92460d7602..2cd5646351 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1922,6 +1922,13 @@ func TestCalcCellValue(t *testing.T) { // EFFECT "=EFFECT(0.1,4)": "0.103812890625", "=EFFECT(0.025,2)": "0.02515625", + // EUROCONVERT + "=EUROCONVERT(1.47,\"EUR\",\"EUR\")": "1.47", + "=EUROCONVERT(1.47,\"EUR\",\"DEM\")": "2.88", + "=EUROCONVERT(1.47,\"FRF\",\"DEM\")": "0.44", + "=EUROCONVERT(1.47,\"FRF\",\"DEM\",FALSE)": "0.44", + "=EUROCONVERT(1.47,\"FRF\",\"DEM\",FALSE,3)": "0.44", + "=EUROCONVERT(1.47,\"FRF\",\"DEM\",TRUE,3)": "0.43810592", // FV "=FV(0.05/12,60,-1000)": "68006.0828408434", "=FV(0.1/4,16,-2000,0,1)": "39729.4608941662", @@ -3958,6 +3965,14 @@ func TestCalcCellValue(t *testing.T) { "=EFFECT(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=EFFECT(0,0)": "#NUM!", "=EFFECT(1,0)": "#NUM!", + // EUROCONVERT + "=EUROCONVERT()": "EUROCONVERT requires at least 3 arguments", + "=EUROCONVERT(1.47,\"FRF\",\"DEM\",TRUE,3,1)": "EUROCONVERT allows at most 5 arguments", + "=EUROCONVERT(\"\",\"FRF\",\"DEM\",TRUE,3)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=EUROCONVERT(1.47,\"FRF\",\"DEM\",\"\",3)": "strconv.ParseBool: parsing \"\": invalid syntax", + "=EUROCONVERT(1.47,\"FRF\",\"DEM\",TRUE,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=EUROCONVERT(1.47,\"\",\"DEM\")": "#VALUE!", + "=EUROCONVERT(1.47,\"FRF\",\"\",TRUE,3)": "#VALUE!", // FV "=FV()": "FV requires at least 3 arguments", "=FV(0,0,0,0,0,0,0)": "FV allows at most 5 arguments", From 2e1b0efadc0519fa4572b2437401bf2993366a07 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 24 Jun 2022 01:03:19 +0800 Subject: [PATCH 065/213] ref #65, new formula function: HYPERLINK --- calc.go | 15 +++++++++++++++ calc_test.go | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/calc.go b/calc.go index c71cd1053c..d9bf653c32 100644 --- a/calc.go +++ b/calc.go @@ -14514,6 +14514,21 @@ func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found") } +// HYPERLINK function creates a hyperlink to a specified location. The syntax +// of the function is: +// +// HYPERLINK(link_location,[friendly_name]) +// +func (fn *formulaFuncs) HYPERLINK(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "HYPERLINK requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "HYPERLINK allows at most 2 arguments") + } + return newStringFormulaArg(argsList.Back().Value.(formulaArg).Value()) +} + // calcMatch returns the position of the value by given match type, criteria // and lookup array for the formula function MATCH. func calcMatch(matchType int, criteria *formulaCriteria, lookupArray []formulaArg) formulaArg { diff --git a/calc_test.go b/calc_test.go index 2cd5646351..b3eb19618b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1788,6 +1788,9 @@ func TestCalcCellValue(t *testing.T) { "=HLOOKUP(F3,F3:F8,3,FALSE)": "34440", "=HLOOKUP(INT(F3),F3:F8,3,FALSE)": "34440", "=HLOOKUP(MUNIT(1),MUNIT(1),1,FALSE)": "1", + // HYPERLINK + "=HYPERLINK(\"https://github.com/xuri/excelize\")": "https://github.com/xuri/excelize", + "=HYPERLINK(\"https://github.com/xuri/excelize\",\"Excelize\")": "Excelize", // VLOOKUP "=VLOOKUP(D2,D:D,1,FALSE)": "Jan", "=VLOOKUP(D2,D1:D10,1)": "Jan", @@ -3725,6 +3728,9 @@ func TestCalcCellValue(t *testing.T) { "=MATCH(0,A1:B1)": "MATCH arguments lookup_array should be one-dimensional array", // TRANSPOSE "=TRANSPOSE()": "TRANSPOSE requires 1 argument", + // HYPERLINK + "=HYPERLINK()": "HYPERLINK requires at least 1 argument", + "=HYPERLINK(\"https://github.com/xuri/excelize\",\"Excelize\",\"\")": "HYPERLINK allows at most 2 arguments", // VLOOKUP "=VLOOKUP()": "VLOOKUP requires at least 3 arguments", "=VLOOKUP(D2,D1,1,FALSE)": "VLOOKUP requires second argument of table array", From 301f7bc21755cdf7c91c9acd50ddcdcf0285f779 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 27 Jun 2022 21:00:59 +0800 Subject: [PATCH 066/213] This closes #1260, fixes compiling issue under 32-bit, and new formula functions - ref #65, new formula functions: DCOUNT and DCOUNTA - support percentile symbol in condition criteria expression - this update dependencies module --- calc.go | 170 +++++++++++++++++++++++++++++++++++++++++++++++++-- calc_test.go | 57 +++++++++++++++++ crypt.go | 8 +-- go.mod | 6 +- go.sum | 14 ++--- 5 files changed, 237 insertions(+), 18 deletions(-) diff --git a/calc.go b/calc.go index d9bf653c32..1d4e96e1f3 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 b3eb19618b..7cf9e48707 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 239208db1e..b00ccdf7b3 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 b08e3d209f..4d628fcf0f 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 db6f6ad1a5..3ffe339485 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= From eee6607e477f229d2459628324cbfae5549f611a Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 28 Jun 2022 23:18:48 +0800 Subject: [PATCH 067/213] ref #65, new formula functions: DMAX and DMIN --- calc.go | 48 +++++++++++++++++++++++++++++++++++++++++++++++- calc_test.go | 10 +++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index 1d4e96e1f3..2fe73b3325 100644 --- a/calc.go +++ b/calc.go @@ -433,6 +433,8 @@ type formulaFuncs struct { // DELTA // DEVSQ // DISC +// DMAX +// DMIN // DOLLARDE // DOLLARFR // DURATION @@ -18098,7 +18100,7 @@ func (fn *formulaFuncs) dcount(name string, argsList *list.List) formulaArg { return newNumberFormulaArg(count) } -// DOUNT function returns the number of cells containing numeric values, in a +// DCOUNT 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: @@ -18119,3 +18121,47 @@ func (fn *formulaFuncs) DCOUNT(argsList *list.List) formulaArg { func (fn *formulaFuncs) DCOUNTA(argsList *list.List) formulaArg { return fn.dcount("DCOUNTA", argsList) } + +// dmaxmin is an implementation of the formula functions DMAX and DMIN. +func (fn *formulaFuncs) dmaxmin(name string, argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 arguments", name)) + } + database := argsList.Front().Value.(formulaArg) + field := argsList.Front().Next().Value.(formulaArg) + criteria := argsList.Back().Value.(formulaArg) + db := newCalcDatabase(database, field, criteria) + if db == nil { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args := list.New() + for db.next() { + args.PushBack(db.value()) + } + if name == "DMAX" { + return fn.MAX(args) + } + return fn.MIN(args) +} + +// DMAX function finds the maximum value in a field (column) in a database for +// selected records only. The records to be included in the calculation are +// defined by a set of one or more user-specified criteria. The syntax of the +// function is: +// +// DMAX(database,field,criteria) +// +func (fn *formulaFuncs) DMAX(argsList *list.List) formulaArg { + return fn.dmaxmin("DMAX", argsList) +} + +// DMIN function finds the minimum value in a field (column) in a database for +// selected records only. The records to be included in the calculation are +// defined by a set of one or more user-specified criteria. The syntax of the +// function is: +// +// DMIN(database,field,criteria) +// +func (fn *formulaFuncs) DMIN(argsList *list.List) formulaArg { + return fn.dmaxmin("DMIN", argsList) +} diff --git a/calc_test.go b/calc_test.go index 7cf9e48707..089bfd3f5e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4603,7 +4603,7 @@ func TestCalcCOVAR(t *testing.T) { } } -func TestCalcDCOUNTandDCOUNTA(t *testing.T) { +func TestCalcDCOUNTandDCOUNTAandDMAXandDMIN(t *testing.T) { cellData := [][]interface{}{ {"Tree", "Height", "Age", "Yield", "Profit", "Height"}, {"=Apple", ">1000%", nil, nil, nil, "<16"}, @@ -4631,6 +4631,10 @@ func TestCalcDCOUNTandDCOUNTA(t *testing.T) { "=DCOUNTA(A4:E10,\"Profit\",A1:F2)": "2", "=DCOUNTA(A4:E10,\"Tree\",A1:F2)": "2", "=DCOUNTA(A4:E10,\"Age\",A2:F3)": "0", + "=DMAX(A4:E10,\"Tree\",A1:F3)": "0", + "=DMAX(A4:E10,\"Profit\",A1:F3)": "96", + "=DMIN(A4:E10,\"Tree\",A1:F3)": "0", + "=DMIN(A4:E10,\"Profit\",A1:F3)": "45", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "A11", formula)) @@ -4651,6 +4655,10 @@ func TestCalcDCOUNTandDCOUNTA(t *testing.T) { "=DCOUNTA(A4:E10,NA(),A1:F2)": "#VALUE!", "=DCOUNTA(A4:E4,,A1:F2)": "#VALUE!", "=DCOUNTA(A4:E10,\"x\",A2:F3)": "#VALUE!", + "=DMAX()": "DMAX requires 3 arguments", + "=DMAX(A4:E10,\"x\",A1:F3)": "#VALUE!", + "=DMIN()": "DMIN requires 3 arguments", + "=DMIN(A4:E10,\"x\",A1:F3)": "#VALUE!", } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "A11", formula)) From dd6c3905e0eadd7d02a1c0d90499d27e465216d2 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 1 Jul 2022 00:43:27 +0800 Subject: [PATCH 068/213] ref #65, new formula function: DAVERAGE --- .gitignore | 1 + calc.go | 85 +++++++++++++++++++++++++++++----------------------- calc_test.go | 44 ++++++++++++++------------- 3 files changed, 73 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index e697544817..44b8b09b45 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ test/excelize-* *.out *.test .idea +.DS_Store diff --git a/calc.go b/calc.go index 2fe73b3325..8476f8d411 100644 --- a/calc.go +++ b/calc.go @@ -418,6 +418,7 @@ type formulaFuncs struct { // DATE // DATEDIF // DATEVALUE +// DAVERAGE // DAY // DAYS // DAYS360 @@ -6008,7 +6009,7 @@ func (fn *formulaFuncs) AVERAGE(argsList *list.List) formulaArg { } count, sum := fn.countSum(false, args) if count == 0 { - return newErrorFormulaArg(formulaErrorDIV, "AVERAGE divide by zero") + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } return newNumberFormulaArg(sum / count) } @@ -6025,7 +6026,7 @@ func (fn *formulaFuncs) AVERAGEA(argsList *list.List) formulaArg { } count, sum := fn.countSum(true, args) if count == 0 { - return newErrorFormulaArg(formulaErrorDIV, "AVERAGEA divide by zero") + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } return newNumberFormulaArg(sum / count) } @@ -6075,7 +6076,7 @@ func (fn *formulaFuncs) AVERAGEIF(argsList *list.List) formulaArg { } count, sum := fn.countSum(false, args) if count == 0 { - return newErrorFormulaArg(formulaErrorDIV, "AVERAGEIF divide by zero") + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } return newNumberFormulaArg(sum / count) } @@ -18068,6 +18069,42 @@ func (db *calcDatabase) next() bool { return matched } +// database is an implementation of the formula functions DAVERAGE, DMAX and DMIN. +func (fn *formulaFuncs) database(name string, argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 arguments", name)) + } + database := argsList.Front().Value.(formulaArg) + field := argsList.Front().Next().Value.(formulaArg) + criteria := argsList.Back().Value.(formulaArg) + db := newCalcDatabase(database, field, criteria) + if db == nil { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args := list.New() + for db.next() { + args.PushBack(db.value()) + } + switch name { + case "DMAX": + return fn.MAX(args) + case "DMIN": + return fn.MIN(args) + default: + return fn.AVERAGE(args) + } +} + +// DAVERAGE function calculates the average (statistical mean) of values in a +// field (column) in a database for selected records, that satisfy +// user-specified criteria. The syntax of the Excel Daverage function is: +// +// DAVERAGE(database,field,criteria) +// +func (fn *formulaFuncs) DAVERAGE(argsList *list.List) formulaArg { + return fn.database("DAVERAGE", argsList) +} + // 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 { @@ -18081,23 +18118,19 @@ func (fn *formulaFuncs) dcount(name string, argsList *list.List) 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) } + args := list.New() for db.next() { - cell := db.value() - if cell.Value() == "" { - continue - } - if num := cell.ToNumber(); name == "DCOUNT" && num.Type != ArgNumber { - continue - } - count++ + args.PushBack(db.value()) } - return newNumberFormulaArg(count) + if name == "DCOUNT" { + return fn.COUNT(args) + } + return fn.COUNTA(args) } // DCOUNT function returns the number of cells containing numeric values, in a @@ -18122,28 +18155,6 @@ func (fn *formulaFuncs) DCOUNTA(argsList *list.List) formulaArg { return fn.dcount("DCOUNTA", argsList) } -// dmaxmin is an implementation of the formula functions DMAX and DMIN. -func (fn *formulaFuncs) dmaxmin(name string, argsList *list.List) formulaArg { - if argsList.Len() != 3 { - return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 arguments", name)) - } - database := argsList.Front().Value.(formulaArg) - field := argsList.Front().Next().Value.(formulaArg) - criteria := argsList.Back().Value.(formulaArg) - db := newCalcDatabase(database, field, criteria) - if db == nil { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args := list.New() - for db.next() { - args.PushBack(db.value()) - } - if name == "DMAX" { - return fn.MAX(args) - } - return fn.MIN(args) -} - // DMAX function finds the maximum value in a field (column) in a database for // selected records only. The records to be included in the calculation are // defined by a set of one or more user-specified criteria. The syntax of the @@ -18152,7 +18163,7 @@ func (fn *formulaFuncs) dmaxmin(name string, argsList *list.List) formulaArg { // DMAX(database,field,criteria) // func (fn *formulaFuncs) DMAX(argsList *list.List) formulaArg { - return fn.dmaxmin("DMAX", argsList) + return fn.database("DMAX", argsList) } // DMIN function finds the minimum value in a field (column) in a database for @@ -18163,5 +18174,5 @@ func (fn *formulaFuncs) DMAX(argsList *list.List) formulaArg { // DMIN(database,field,criteria) // func (fn *formulaFuncs) DMIN(argsList *list.List) formulaArg { - return fn.dmaxmin("DMIN", argsList) + return fn.database("DMIN", argsList) } diff --git a/calc_test.go b/calc_test.go index 089bfd3f5e..8ad3c775f7 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2655,14 +2655,14 @@ func TestCalcCellValue(t *testing.T) { "=AVEDEV(\"\")": "#VALUE!", "=AVEDEV(1,\"\")": "#VALUE!", // AVERAGE - "=AVERAGE(H1)": "AVERAGE divide by zero", + "=AVERAGE(H1)": "#DIV/0!", // AVERAGEA - "=AVERAGEA(H1)": "AVERAGEA divide by zero", + "=AVERAGEA(H1)": "#DIV/0!", // AVERAGEIF "=AVERAGEIF()": "AVERAGEIF requires at least 2 arguments", - "=AVERAGEIF(H1,\"\")": "AVERAGEIF divide by zero", - "=AVERAGEIF(D1:D3,\"Month\",D1:D3)": "AVERAGEIF divide by zero", - "=AVERAGEIF(C1:C3,\"Month\",D1:D3)": "AVERAGEIF divide by zero", + "=AVERAGEIF(H1,\"\")": "#DIV/0!", + "=AVERAGEIF(D1:D3,\"Month\",D1:D3)": "#DIV/0!", + "=AVERAGEIF(C1:C3,\"Month\",D1:D3)": "#DIV/0!", // BETA.DIST "=BETA.DIST()": "BETA.DIST requires at least 4 arguments", "=BETA.DIST(0.4,4,5,TRUE,0,1,0)": "BETA.DIST requires at most 6 arguments", @@ -4603,7 +4603,7 @@ func TestCalcCOVAR(t *testing.T) { } } -func TestCalcDCOUNTandDCOUNTAandDMAXandDMIN(t *testing.T) { +func TestCalcDatabase(t *testing.T) { cellData := [][]interface{}{ {"Tree", "Height", "Age", "Yield", "Profit", "Height"}, {"=Apple", ">1000%", nil, nil, nil, "<16"}, @@ -4621,20 +4621,21 @@ func TestCalcDCOUNTandDCOUNTAandDMAXandDMIN(t *testing.T) { 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", - "=DMAX(A4:E10,\"Tree\",A1:F3)": "0", - "=DMAX(A4:E10,\"Profit\",A1:F3)": "96", - "=DMIN(A4:E10,\"Tree\",A1:F3)": "0", - "=DMIN(A4:E10,\"Profit\",A1:F3)": "45", + "=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", + "=DMAX(A4:E10,\"Tree\",A1:F3)": "0", + "=DMAX(A4:E10,\"Profit\",A1:F3)": "96", + "=DMIN(A4:E10,\"Tree\",A1:F3)": "0", + "=DMIN(A4:E10,\"Profit\",A1:F3)": "45", + "=DAVERAGE(A4:E10,\"Profit\",A1:F3)": "73.25", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "A11", formula)) @@ -4659,6 +4660,9 @@ func TestCalcDCOUNTandDCOUNTAandDMAXandDMIN(t *testing.T) { "=DMAX(A4:E10,\"x\",A1:F3)": "#VALUE!", "=DMIN()": "DMIN requires 3 arguments", "=DMIN(A4:E10,\"x\",A1:F3)": "#VALUE!", + "=DAVERAGE()": "DAVERAGE requires 3 arguments", + "=DAVERAGE(A4:E10,\"x\",A1:F3)": "#VALUE!", + "=DAVERAGE(A4:E10,\"Tree\",A1:F3)": "#DIV/0!", } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "A11", formula)) From 18afc88759558478cb6d433e725ccd10d4474f7e Mon Sep 17 00:00:00 2001 From: yeshu <673643706@qq.com> Date: Fri, 1 Jul 2022 00:46:23 +0800 Subject: [PATCH 069/213] This closes #1264, fix can't modify cell content issue in some cases Remove inline rich text when setting cell value and cell formulas --- cell.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cell.go b/cell.go index 6d4c62b92d..286085b3ab 100644 --- a/cell.go +++ b/cell.go @@ -266,6 +266,7 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellInt(value) + cellData.F, cellData.IS = nil, nil return err } @@ -291,6 +292,7 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellBool(value) + cellData.F, cellData.IS = nil, nil return err } @@ -328,6 +330,7 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, precision, bitSiz defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellFloat(value, precision, bitSize) + cellData.F, cellData.IS = nil, nil return err } @@ -353,6 +356,7 @@ func (f *File) SetCellStr(sheet, axis, value string) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V, err = f.setCellString(value) + cellData.F, cellData.IS = nil, nil return err } @@ -451,6 +455,7 @@ func (f *File) SetCellDefault(sheet, axis, value string) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellDefault(value) + cellData.F, cellData.IS = nil, nil return err } @@ -599,7 +604,7 @@ func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) cellData.F.Ref = *o.Ref } } - + cellData.IS = nil return err } From a77d38f04059d4febd2aba8b5656a826af51d9e7 Mon Sep 17 00:00:00 2001 From: yeshu <673643706@qq.com> Date: Sat, 2 Jul 2022 00:38:24 +0800 Subject: [PATCH 070/213] Fix CONTRIBUTING doc typo issues (#1266) --- CONTRIBUTING.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 847e3ac68d..0c966e4940 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ issue, please bring it to their attention right away! Please **DO NOT** file a public issue, instead send your report privately to [xuri.me](https://xuri.me). -Security reports are greatly appreciated and we will publicly thank you for it. +Security reports are greatly appreciated and we will publicly thank you for them. We currently do not offer a paid security bounty program, but are not ruling it out in the future. @@ -103,14 +103,14 @@ Before contributing large or high impact changes, make the effort to coordinate with the maintainers of the project before submitting a pull request. This prevents you from doing extra work that may or may not be merged. -Large PRs that are just submitted without any prior communication are unlikely +Large PRs that are just submitted without any prior communication is unlikely to be successful. While pull requests are the methodology for submitting changes to code, changes are much more likely to be accepted if they are accompanied by additional engineering work. While we don't define this explicitly, most of these goals -are accomplished through communication of the design goals and subsequent -solutions. Often times, it helps to first state the problem before presenting +are accomplished through the communication of the design goals and subsequent +solutions. Oftentimes, it helps to first state the problem before presenting solutions. Typically, the best methods of accomplishing this are to submit an issue, @@ -130,7 +130,7 @@ written in the imperative, followed by an optional, more detailed explanatory text which is separated from the summary by an empty line. Commit messages should follow best practices, including explaining the context -of the problem and how it was solved, including in caveats or follow up changes +of the problem and how it was solved, including in caveats or follow-up changes required. They should tell the story of the change and provide readers understanding of what led to it. @@ -260,7 +260,7 @@ Don't forget: being a maintainer is a time investment. Make sure you will have time to make yourself available. You don't have to be a maintainer to make a difference on the project! -If you want to become a meintainer, contact [xuri.me](https://xuri.me) and given a introduction of you. +If you want to become a maintainer, contact [xuri.me](https://xuri.me) and given an introduction of you. ## Community guidelines @@ -414,7 +414,7 @@ The first sentence should be a one-sentence summary that starts with the name be It's helpful if everyone using the package can use the same name to refer to its contents, which implies that the package name should -be good: short, concise, evocative. By convention, packages are +be good: short, concise, and evocative. By convention, packages are given lower case, single-word names; there should be no need for underscores or mixedCaps. Err on the side of brevity, since everyone using your package will be typing that name. And don't worry about From 695db4eae06fdc2a049fc50f61e6a60f83013290 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 2 Jul 2022 13:43:31 +0800 Subject: [PATCH 071/213] ref #65, new formula functions: DPRODUCT, DSTDEV, DSTDEVP, DSUM, DVAR, and DVARP --- calc.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 26 ++++++++++++++--- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index 8476f8d411..25c2e180d6 100644 --- a/calc.go +++ b/calc.go @@ -438,7 +438,13 @@ type formulaFuncs struct { // DMIN // DOLLARDE // DOLLARFR +// DPRODUCT +// DSTDEV +// DSTDEVP +// DSUM // DURATION +// DVAR +// DVARP // EFFECT // EDATE // ENCODEURL @@ -18090,6 +18096,18 @@ func (fn *formulaFuncs) database(name string, argsList *list.List) formulaArg { return fn.MAX(args) case "DMIN": return fn.MIN(args) + case "DPRODUCT": + return fn.PRODUCT(args) + case "DSTDEV": + return fn.STDEV(args) + case "DSTDEVP": + return fn.STDEVP(args) + case "DSUM": + return fn.SUM(args) + case "DVAR": + return fn.VAR(args) + case "DVARP": + return fn.VARP(args) default: return fn.AVERAGE(args) } @@ -18176,3 +18194,67 @@ func (fn *formulaFuncs) DMAX(argsList *list.List) formulaArg { func (fn *formulaFuncs) DMIN(argsList *list.List) formulaArg { return fn.database("DMIN", argsList) } + +// DPRODUCT function calculates the product of a field (column) in a database +// for selected records, that satisfy user-specified criteria. The syntax of +// the function is: +// +// DPRODUCT(database,field,criteria) +// +func (fn *formulaFuncs) DPRODUCT(argsList *list.List) formulaArg { + return fn.database("DPRODUCT", argsList) +} + +// DSTDEV function calculates the sample standard deviation of a field +// (column) in a database for selected records only. The records to be +// included in the calculation are defined by a set of one or more +// user-specified criteria. The syntax of the function is: +// +// DSTDEV(database,field,criteria) +// +func (fn *formulaFuncs) DSTDEV(argsList *list.List) formulaArg { + return fn.database("DSTDEV", argsList) +} + +// DSTDEVP function calculates the standard deviation of a field (column) in a +// database for selected records only. The records to be included in the +// calculation are defined by a set of one or more user-specified criteria. +// The syntax of the function is: +// +// DSTDEVP(database,field,criteria) +// +func (fn *formulaFuncs) DSTDEVP(argsList *list.List) formulaArg { + return fn.database("DSTDEVP", argsList) +} + +// DSUM function calculates the sum of a field (column) in a database for +// selected records, that satisfy user-specified criteria. The syntax of the +// function is: +// +// DSUM(database,field,criteria) +// +func (fn *formulaFuncs) DSUM(argsList *list.List) formulaArg { + return fn.database("DSUM", argsList) +} + +// DVAR function calculates the sample variance of a field (column) in a +// database for selected records only. The records to be included in the +// calculation are defined by a set of one or more user-specified criteria. +// The syntax of the function is: +// +// DVAR(database,field,criteria) +// +func (fn *formulaFuncs) DVAR(argsList *list.List) formulaArg { + return fn.database("DVAR", argsList) +} + +// DVARP function calculates the variance (for an entire population), of the +// values in a field (column) in a database for selected records only. The +// records to be included in the calculation are defined by a set of one or +// more user-specified criteria. The syntax of the function is: +// +// DVARP(database,field,criteria) +// +func (fn *formulaFuncs) DVARP(argsList *list.List) formulaArg { + return fn.database("DVARP", argsList) +} diff --git a/calc_test.go b/calc_test.go index 8ad3c775f7..4436b6081f 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4621,6 +4621,7 @@ func TestCalcDatabase(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "=\"=Pear\"")) assert.NoError(t, f.SetCellFormula("Sheet1", "C8", "=NA()")) formulaList := map[string]string{ + "=DAVERAGE(A4:E10,\"Profit\",A1:F3)": "73.25", "=DCOUNT(A4:E10,\"Age\",A1:F2)": "1", "=DCOUNT(A4:E10,,A1:F2)": "2", "=DCOUNT(A4:E10,\"Profit\",A1:F2)": "2", @@ -4635,7 +4636,12 @@ func TestCalcDatabase(t *testing.T) { "=DMAX(A4:E10,\"Profit\",A1:F3)": "96", "=DMIN(A4:E10,\"Tree\",A1:F3)": "0", "=DMIN(A4:E10,\"Profit\",A1:F3)": "45", - "=DAVERAGE(A4:E10,\"Profit\",A1:F3)": "73.25", + "=DPRODUCT(A4:E10,\"Profit\",A1:F3)": "24948000", + "=DSTDEV(A4:E10,\"Profit\",A1:F3)": "21.077238908358", + "=DSTDEVP(A4:E10,\"Profit\",A1:F3)": "18.2534243362718", + "=DSUM(A4:E10,\"Profit\",A1:F3)": "293", + "=DVAR(A4:E10,\"Profit\",A1:F3)": "444.25", + "=DVARP(A4:E10,\"Profit\",A1:F3)": "333.1875", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "A11", formula)) @@ -4644,6 +4650,9 @@ func TestCalcDatabase(t *testing.T) { assert.Equal(t, expected, result, formula) } calcError := map[string]string{ + "=DAVERAGE()": "DAVERAGE requires 3 arguments", + "=DAVERAGE(A4:E10,\"x\",A1:F3)": "#VALUE!", + "=DAVERAGE(A4:E10,\"Tree\",A1:F3)": "#DIV/0!", "=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!", @@ -4660,9 +4669,18 @@ func TestCalcDatabase(t *testing.T) { "=DMAX(A4:E10,\"x\",A1:F3)": "#VALUE!", "=DMIN()": "DMIN requires 3 arguments", "=DMIN(A4:E10,\"x\",A1:F3)": "#VALUE!", - "=DAVERAGE()": "DAVERAGE requires 3 arguments", - "=DAVERAGE(A4:E10,\"x\",A1:F3)": "#VALUE!", - "=DAVERAGE(A4:E10,\"Tree\",A1:F3)": "#DIV/0!", + "=DPRODUCT()": "DPRODUCT requires 3 arguments", + "=DPRODUCT(A4:E10,\"x\",A1:F3)": "#VALUE!", + "=DSTDEV()": "DSTDEV requires 3 arguments", + "=DSTDEV(A4:E10,\"x\",A1:F3)": "#VALUE!", + "=DSTDEVP()": "DSTDEVP requires 3 arguments", + "=DSTDEVP(A4:E10,\"x\",A1:F3)": "#VALUE!", + "=DSUM()": "DSUM requires 3 arguments", + "=DSUM(A4:E10,\"x\",A1:F3)": "#VALUE!", + "=DVAR()": "DVAR requires 3 arguments", + "=DVAR(A4:E10,\"x\",A1:F3)": "#VALUE!", + "=DVARP()": "DVARP requires 3 arguments", + "=DVARP(A4:E10,\"x\",A1:F3)": "#VALUE!", } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "A11", formula)) From d74adcbb159280be962918125d20ec8ac67a3f93 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 3 Jul 2022 15:31:24 +0800 Subject: [PATCH 072/213] ref #65, new formula function: DGET --- calc.go | 27 +++++++++++++++++++++++++++ calc_test.go | 4 ++++ 2 files changed, 31 insertions(+) diff --git a/calc.go b/calc.go index 25c2e180d6..83885dc6ba 100644 --- a/calc.go +++ b/calc.go @@ -433,6 +433,7 @@ type formulaFuncs struct { // DEGREES // DELTA // DEVSQ +// DGET // DISC // DMAX // DMIN @@ -18173,6 +18174,32 @@ func (fn *formulaFuncs) DCOUNTA(argsList *list.List) formulaArg { return fn.dcount("DCOUNTA", argsList) } +// DGET function returns a single value from a column of a database. The record +// is selected via a set of one or more user-specified criteria. The syntax of +// the function is: +// +// DGET(database,field,criteria) +// +func (fn *formulaFuncs) DGET(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "DGET requires 3 arguments") + } + database := argsList.Front().Value.(formulaArg) + field := argsList.Front().Next().Value.(formulaArg) + criteria := argsList.Back().Value.(formulaArg) + db := newCalcDatabase(database, field, criteria) + if db == nil { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + value := newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + if db.next() { + if value = db.value(); db.next() { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + return value +} + // DMAX function finds the maximum value in a field (column) in a database for // selected records only. The records to be included in the calculation are // defined by a set of one or more user-specified criteria. The syntax of the diff --git a/calc_test.go b/calc_test.go index 4436b6081f..b8efd61912 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4632,6 +4632,7 @@ func TestCalcDatabase(t *testing.T) { "=DCOUNTA(A4:E10,\"Profit\",A1:F2)": "2", "=DCOUNTA(A4:E10,\"Tree\",A1:F2)": "2", "=DCOUNTA(A4:E10,\"Age\",A2:F3)": "0", + "=DGET(A4:E6,\"Profit\",A1:F3)": "96", "=DMAX(A4:E10,\"Tree\",A1:F3)": "0", "=DMAX(A4:E10,\"Profit\",A1:F3)": "96", "=DMIN(A4:E10,\"Tree\",A1:F3)": "0", @@ -4665,6 +4666,9 @@ func TestCalcDatabase(t *testing.T) { "=DCOUNTA(A4:E10,NA(),A1:F2)": "#VALUE!", "=DCOUNTA(A4:E4,,A1:F2)": "#VALUE!", "=DCOUNTA(A4:E10,\"x\",A2:F3)": "#VALUE!", + "=DGET()": "DGET requires 3 arguments", + "=DGET(A4:E5,\"Profit\",A1:F3)": "#VALUE!", + "=DGET(A4:E10,\"Profit\",A1:F3)": "#NUM!", "=DMAX()": "DMAX requires 3 arguments", "=DMAX(A4:E10,\"x\",A1:F3)": "#VALUE!", "=DMIN()": "DMIN requires 3 arguments", From 1dbed64f105db2a715d963933642839460b6642a Mon Sep 17 00:00:00 2001 From: Eagle Xiang Date: Wed, 6 Jul 2022 20:39:10 +0800 Subject: [PATCH 073/213] This closes #1269, made the `NewStreamWriter` function case insensitive to worksheet name Co-authored-by: xiangyz --- sheet.go | 2 +- sheet_test.go | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/sheet.go b/sheet.go index 45b724f7bb..e88f9536e5 100644 --- a/sheet.go +++ b/sheet.go @@ -376,7 +376,7 @@ func (f *File) GetSheetName(index int) (name string) { // integer type value -1. func (f *File) getSheetID(name string) int { for sheetID, sheet := range f.GetSheetMap() { - if sheet == trimSheetName(name) { + if strings.EqualFold(sheet, trimSheetName(name)) { return sheetID } } diff --git a/sheet_test.go b/sheet_test.go index 3ad0e75245..c68ad3129f 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -465,6 +465,13 @@ func TestDeleteAndAdjustDefinedNames(t *testing.T) { deleteAndAdjustDefinedNames(&xlsxWorkbook{}, 0) } +func TestGetSheetID(t *testing.T) { + file := NewFile() + file.NewSheet("Sheet1") + id := file.getSheetID("sheet1") + assert.NotEqual(t, -1, id) +} + func BenchmarkNewSheet(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { From a65c5846e45fece382f72465f9e858c788dfcfef Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 10 Jul 2022 18:14:48 +0800 Subject: [PATCH 074/213] This closes #1262, support for dependence formulas calculation - Add export option `MaxCalcIterations` for specifies the maximum iterations for iterative calculation - Update unit test for the database formula functions --- calc.go | 69 ++++++++++++++++++++++++++++++++++++++-------------- calc_test.go | 4 +-- excelize.go | 4 +++ 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/calc.go b/calc.go index 83885dc6ba..78846d23a1 100644 --- a/calc.go +++ b/calc.go @@ -26,6 +26,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" "unicode" "unsafe" @@ -193,6 +194,13 @@ var ( } ) +// calcContext defines the formula execution context. +type calcContext struct { + sync.Mutex + entry string + iterations map[string]uint +} + // cellRef defines the structure of a cell reference. type cellRef struct { Col int @@ -312,6 +320,7 @@ func (fa formulaArg) ToList() []formulaArg { // formulaFuncs is the type of the formula functions. type formulaFuncs struct { f *File + ctx *calcContext sheet, cell string } @@ -758,6 +767,13 @@ type formulaFuncs struct { // ZTEST // func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { + return f.calcCellValue(&calcContext{ + entry: fmt.Sprintf("%s!%s", sheet, cell), + iterations: make(map[string]uint), + }, sheet, cell) +} + +func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result string, err error) { var ( formula string token formulaArg @@ -770,7 +786,7 @@ func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { if tokens == nil { return } - if token, err = f.evalInfixExp(sheet, cell, tokens); err != nil { + if token, err = f.evalInfixExp(ctx, sheet, cell, tokens); err != nil { return } result = token.Value() @@ -850,7 +866,7 @@ func newEmptyFormulaArg() formulaArg { // // TODO: handle subtypes: Nothing, Text, Logical, Error, Concatenation, Intersection, Union // -func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, error) { +func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.Token) (formulaArg, error) { var err error opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack() var inArray, inArrayRow bool @@ -860,7 +876,7 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, // out of function stack if opfStack.Len() == 0 { - if err = f.parseToken(sheet, token, opdStack, optStack); err != nil { + if err = f.parseToken(ctx, sheet, token, opdStack, optStack); err != nil { return newEmptyFormulaArg(), err } } @@ -896,7 +912,7 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, token.TValue = refTo } // parse reference: must reference at here - result, err := f.parseReference(sheet, token.TValue) + result, err := f.parseReference(ctx, sheet, token.TValue) if err != nil { return result, err } @@ -912,7 +928,7 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, if refTo != "" { token.TValue = refTo } - result, err := f.parseReference(sheet, token.TValue) + result, err := f.parseReference(ctx, sheet, token.TValue) if err != nil { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE), err } @@ -938,7 +954,7 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, } // check current token is opft - if err = f.parseToken(sheet, token, opfdStack, opftStack); err != nil { + if err = f.parseToken(ctx, sheet, token, opfdStack, opftStack); err != nil { return newEmptyFormulaArg(), err } @@ -975,7 +991,7 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, arrayRow, inArray = []formulaArg{}, false continue } - if err = f.evalInfixExpFunc(sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { + if err = f.evalInfixExpFunc(ctx, sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { return newEmptyFormulaArg(), err } } @@ -994,13 +1010,13 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, } // evalInfixExpFunc evaluate formula function in the infix expression. -func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, opfStack, opdStack, opftStack, opfdStack, argsStack *Stack) error { +func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nextToken efp.Token, opfStack, opdStack, opftStack, opfdStack, argsStack *Stack) error { if !isFunctionStopToken(token) { return nil } prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack) // call formula function to evaluate - arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell}, strings.NewReplacer( + arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell, ctx: ctx}, strings.NewReplacer( "_xlfn.", "", ".", "dot").Replace(opfStack.Peek().(efp.Token).TValue), []reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))}) if arg.Type == ArgError && opfStack.Len() == 1 { @@ -1337,14 +1353,14 @@ func tokenToFormulaArg(token efp.Token) formulaArg { // parseToken parse basic arithmetic operator priority and evaluate based on // operators and operands. -func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error { +func (f *File) parseToken(ctx *calcContext, sheet string, token efp.Token, opdStack, optStack *Stack) error { // parse reference: must reference at here if token.TSubType == efp.TokenSubTypeRange { refTo := f.getDefinedNameRefTo(token.TValue, sheet) if refTo != "" { token.TValue = refTo } - result, err := f.parseReference(sheet, token.TValue) + result, err := f.parseReference(ctx, sheet, token.TValue) if err != nil { return errors.New(formulaErrorNAME) } @@ -1386,7 +1402,7 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta // parseReference parse reference and extract values by given reference // characters and default sheet name. -func (f *File) parseReference(sheet, reference string) (arg formulaArg, err error) { +func (f *File) parseReference(ctx *calcContext, sheet, reference string) (arg formulaArg, err error) { reference = strings.ReplaceAll(reference, "$", "") refs, cellRanges, cellRefs := list.New(), list.New(), list.New() for _, ref := range strings.Split(reference, ":") { @@ -1430,7 +1446,7 @@ func (f *File) parseReference(sheet, reference string) (arg formulaArg, err erro To: cellRef{Sheet: sheet, Col: cr.Col, Row: TotalRows}, }) cellRefs.Init() - arg, err = f.rangeResolver(cellRefs, cellRanges) + arg, err = f.rangeResolver(ctx, cellRefs, cellRanges) return } e := refs.Back() @@ -1450,7 +1466,7 @@ func (f *File) parseReference(sheet, reference string) (arg formulaArg, err erro cellRefs.PushBack(e.Value.(cellRef)) refs.Remove(e) } - arg, err = f.rangeResolver(cellRefs, cellRanges) + arg, err = f.rangeResolver(ctx, cellRefs, cellRanges) return } @@ -1486,10 +1502,27 @@ func prepareValueRef(cr cellRef, valueRange []int) { } } +// cellResolver calc cell value by given worksheet name, cell reference and context. +func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (string, error) { + var value string + ref := fmt.Sprintf("%s!%s", sheet, cell) + if formula, _ := f.GetCellFormula(sheet, cell); len(formula) != 0 { + ctx.Lock() + if ctx.entry != ref && ctx.iterations[ref] <= f.options.MaxCalcIterations { + ctx.iterations[ref]++ + ctx.Unlock() + value, _ = f.calcCellValue(ctx, sheet, cell) + return value, nil + } + ctx.Unlock() + } + return f.GetCellValue(sheet, cell, Options{RawCellValue: true}) +} + // rangeResolver extract value as string from given reference and range list. // This function will not ignore the empty cell. For example, A1:A2:A2:B3 will // be reference A1:B3. -func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (arg formulaArg, err error) { +func (f *File) rangeResolver(ctx *calcContext, cellRefs, cellRanges *list.List) (arg formulaArg, err error) { arg.cellRefs, arg.cellRanges = cellRefs, cellRanges // value range order: from row, to row, from column, to column valueRange := []int{0, 0, 0, 0} @@ -1525,7 +1558,7 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (arg formulaArg, e if cell, err = CoordinatesToCellName(col, row); err != nil { return } - if value, err = f.GetCellValue(sheet, cell, Options{RawCellValue: true}); err != nil { + if value, err = f.cellResolver(ctx, sheet, cell); err != nil { return } matrixRow = append(matrixRow, formulaArg{ @@ -1544,7 +1577,7 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (arg formulaArg, e if cell, err = CoordinatesToCellName(cr.Col, cr.Row); err != nil { return } - if arg.String, err = f.GetCellValue(cr.Sheet, cell, Options{RawCellValue: true}); err != nil { + if arg.String, err = f.cellResolver(ctx, cr.Sheet, cell); err != nil { return } arg.Type = ArgString @@ -15092,7 +15125,7 @@ func (fn *formulaFuncs) INDIRECT(argsList *list.List) formulaArg { } return newStringFormulaArg(value) } - arg, _ := fn.f.parseReference(fn.sheet, fromRef+":"+toRef) + arg, _ := fn.f.parseReference(fn.ctx, fn.sheet, fromRef+":"+toRef) return arg } diff --git a/calc_test.go b/calc_test.go index b8efd61912..47cd8067b2 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4606,8 +4606,8 @@ func TestCalcCOVAR(t *testing.T) { func TestCalcDatabase(t *testing.T) { cellData := [][]interface{}{ {"Tree", "Height", "Age", "Yield", "Profit", "Height"}, - {"=Apple", ">1000%", nil, nil, nil, "<16"}, - {"=Pear"}, + {nil, ">1000%", nil, nil, nil, "<16"}, + {}, {"Tree", "Height", "Age", "Yield", "Profit"}, {"Apple", 18, 20, 14, 105}, {"Pear", 12, 12, 10, 96}, diff --git a/excelize.go b/excelize.go index 580bc292ed..da51b13c81 100644 --- a/excelize.go +++ b/excelize.go @@ -63,6 +63,9 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e // Options define the options for open and reading spreadsheet. // +// MaxCalcIterations specifies the maximum iterations for iterative +// calculation, the default value is 0. +// // Password specifies the password of the spreadsheet in plain text. // // RawCellValue specifies if apply the number format for the cell value or get @@ -78,6 +81,7 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e // should be less than or equal to UnzipSizeLimit, the default value is // 16MB. type Options struct { + MaxCalcIterations uint Password string RawCellValue bool UnzipSizeLimit int64 From e37724c22b95de974f0235e992236d555aa6ad12 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 14 Jul 2022 00:17:51 +0800 Subject: [PATCH 075/213] This fix potential panic and file corrupted - Fix the panic when set or get sheet view options on the sheet without views options - Fix generated workbook corruption caused by empty created or modified dcterms in the document core properties - Update the unit tests --- docProps.go | 20 +++++++---- sheetview.go | 5 +++ sheetview_test.go | 5 +++ xmlCore.go | 92 +++++++++++++++++++++++------------------------ 4 files changed, 70 insertions(+), 52 deletions(-) diff --git a/docProps.go b/docProps.go index 41ea18e258..fe6f21447b 100644 --- a/docProps.go +++ b/docProps.go @@ -204,7 +204,12 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { Category: core.Category, Version: core.Version, }, nil - newProps.Created.Text, newProps.Created.Type, newProps.Modified.Text, newProps.Modified.Type = core.Created.Text, core.Created.Type, core.Modified.Text, core.Modified.Type + if core.Created != nil { + newProps.Created = &xlsxDcTerms{Type: core.Created.Type, Text: core.Created.Text} + } + if core.Modified != nil { + newProps.Modified = &xlsxDcTerms{Type: core.Modified.Type, Text: core.Modified.Text} + } fields = []string{ "Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords", "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version", @@ -216,10 +221,10 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { } } if docProperties.Created != "" { - newProps.Created.Text = docProperties.Created + newProps.Created = &xlsxDcTerms{Type: "dcterms:W3CDTF", Text: docProperties.Created} } if docProperties.Modified != "" { - newProps.Modified.Text = docProperties.Modified + newProps.Modified = &xlsxDcTerms{Type: "dcterms:W3CDTF", Text: docProperties.Modified} } output, err = xml.Marshal(newProps) f.saveFileList(defaultXMLPathDocPropsCore, output) @@ -239,19 +244,22 @@ func (f *File) GetDocProps() (ret *DocProperties, err error) { ret, err = &DocProperties{ Category: core.Category, ContentStatus: core.ContentStatus, - Created: core.Created.Text, Creator: core.Creator, Description: core.Description, Identifier: core.Identifier, Keywords: core.Keywords, LastModifiedBy: core.LastModifiedBy, - Modified: core.Modified.Text, Revision: core.Revision, Subject: core.Subject, Title: core.Title, Language: core.Language, Version: core.Version, }, nil - + if core.Created != nil { + ret.Created = core.Created.Text + } + if core.Modified != nil { + ret.Modified = core.Modified.Text + } return } diff --git a/sheetview.go b/sheetview.go index bf8f0237ba..99f0634ff0 100644 --- a/sheetview.go +++ b/sheetview.go @@ -163,6 +163,11 @@ func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) if err != nil { return nil, err } + if ws.SheetViews == nil { + ws.SheetViews = &xlsxSheetViews{ + SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, + } + } if viewIndex < 0 { if viewIndex < -len(ws.SheetViews.SheetView) { return nil, fmt.Errorf("view index %d out of range", viewIndex) diff --git a/sheetview_test.go b/sheetview_test.go index 2bba8f9802..65c4f5120b 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -210,4 +210,9 @@ func TestSheetViewOptionsErrors(t *testing.T) { assert.NoError(t, f.SetSheetViewOptions(sheet, -1)) assert.Error(t, f.SetSheetViewOptions(sheet, 1)) assert.Error(t, f.SetSheetViewOptions(sheet, -2)) + + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetViews = nil + assert.NoError(t, f.GetSheetViewOptions(sheet, 0)) } diff --git a/xmlCore.go b/xmlCore.go index 22ae6bc088..18491319be 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -31,61 +31,61 @@ type DocProperties struct { Version string } +// decodeDcTerms directly maps the DCMI metadata terms for the coreProperties. +type decodeDcTerms struct { + Text string `xml:",chardata"` + Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"` +} + // decodeCoreProperties directly maps the root element for a part of this // content type shall coreProperties. In order to solve the problem that the // label structure is changed after serialization and deserialization, two // different structures are defined. decodeCoreProperties just for // deserialization. type decodeCoreProperties struct { - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/metadata/core-properties coreProperties"` - Title string `xml:"http://purl.org/dc/elements/1.1/ title,omitempty"` - Subject string `xml:"http://purl.org/dc/elements/1.1/ subject,omitempty"` - Creator string `xml:"http://purl.org/dc/elements/1.1/ creator"` - Keywords string `xml:"keywords,omitempty"` - Description string `xml:"http://purl.org/dc/elements/1.1/ description,omitempty"` - LastModifiedBy string `xml:"lastModifiedBy"` - Language string `xml:"http://purl.org/dc/elements/1.1/ language,omitempty"` - Identifier string `xml:"http://purl.org/dc/elements/1.1/ identifier,omitempty"` - Revision string `xml:"revision,omitempty"` - Created struct { - Text string `xml:",chardata"` - Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"` - } `xml:"http://purl.org/dc/terms/ created"` - Modified struct { - Text string `xml:",chardata"` - Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"` - } `xml:"http://purl.org/dc/terms/ modified"` - ContentStatus string `xml:"contentStatus,omitempty"` - Category string `xml:"category,omitempty"` - Version string `xml:"version,omitempty"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/metadata/core-properties coreProperties"` + Title string `xml:"http://purl.org/dc/elements/1.1/ title,omitempty"` + Subject string `xml:"http://purl.org/dc/elements/1.1/ subject,omitempty"` + Creator string `xml:"http://purl.org/dc/elements/1.1/ creator"` + Keywords string `xml:"keywords,omitempty"` + Description string `xml:"http://purl.org/dc/elements/1.1/ description,omitempty"` + LastModifiedBy string `xml:"lastModifiedBy"` + Language string `xml:"http://purl.org/dc/elements/1.1/ language,omitempty"` + Identifier string `xml:"http://purl.org/dc/elements/1.1/ identifier,omitempty"` + Revision string `xml:"revision,omitempty"` + Created *decodeDcTerms `xml:"http://purl.org/dc/terms/ created"` + Modified *decodeDcTerms `xml:"http://purl.org/dc/terms/ modified"` + ContentStatus string `xml:"contentStatus,omitempty"` + Category string `xml:"category,omitempty"` + Version string `xml:"version,omitempty"` +} + +// xlsxDcTerms directly maps the DCMI metadata terms for the coreProperties. +type xlsxDcTerms struct { + Text string `xml:",chardata"` + Type string `xml:"xsi:type,attr"` } // xlsxCoreProperties directly maps the root element for a part of this // content type shall coreProperties. type xlsxCoreProperties struct { - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/metadata/core-properties coreProperties"` - Dc string `xml:"xmlns:dc,attr"` - Dcterms string `xml:"xmlns:dcterms,attr"` - Dcmitype string `xml:"xmlns:dcmitype,attr"` - XSI string `xml:"xmlns:xsi,attr"` - Title string `xml:"dc:title,omitempty"` - Subject string `xml:"dc:subject,omitempty"` - Creator string `xml:"dc:creator"` - Keywords string `xml:"keywords,omitempty"` - Description string `xml:"dc:description,omitempty"` - LastModifiedBy string `xml:"lastModifiedBy"` - Language string `xml:"dc:language,omitempty"` - Identifier string `xml:"dc:identifier,omitempty"` - Revision string `xml:"revision,omitempty"` - Created struct { - Text string `xml:",chardata"` - Type string `xml:"xsi:type,attr"` - } `xml:"dcterms:created"` - Modified struct { - Text string `xml:",chardata"` - Type string `xml:"xsi:type,attr"` - } `xml:"dcterms:modified"` - ContentStatus string `xml:"contentStatus,omitempty"` - Category string `xml:"category,omitempty"` - Version string `xml:"version,omitempty"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/metadata/core-properties coreProperties"` + Dc string `xml:"xmlns:dc,attr"` + Dcterms string `xml:"xmlns:dcterms,attr"` + Dcmitype string `xml:"xmlns:dcmitype,attr"` + XSI string `xml:"xmlns:xsi,attr"` + Title string `xml:"dc:title,omitempty"` + Subject string `xml:"dc:subject,omitempty"` + Creator string `xml:"dc:creator"` + Keywords string `xml:"keywords,omitempty"` + Description string `xml:"dc:description,omitempty"` + LastModifiedBy string `xml:"lastModifiedBy"` + Language string `xml:"dc:language,omitempty"` + Identifier string `xml:"dc:identifier,omitempty"` + Revision string `xml:"revision,omitempty"` + Created *xlsxDcTerms `xml:"dcterms:created"` + Modified *xlsxDcTerms `xml:"dcterms:modified"` + ContentStatus string `xml:"contentStatus,omitempty"` + Category string `xml:"category,omitempty"` + Version string `xml:"version,omitempty"` } From 6429588e1448f70539774a88840f094829cb9e07 Mon Sep 17 00:00:00 2001 From: MJacred Date: Thu, 14 Jul 2022 17:36:43 +0200 Subject: [PATCH 076/213] adjust `ErrColumnNumber`, rename `TotalColumns` to `MaxColumns` and add new constant `MinColumns` (#1272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Benjamin Lösch --- calc.go | 8 ++++---- errors.go | 10 +++++----- lib.go | 7 ++----- lib_test.go | 6 +++--- sheet.go | 2 +- stream.go | 5 +---- stream_test.go | 6 +++--- xmlDrawing.go | 3 ++- 8 files changed, 21 insertions(+), 26 deletions(-) diff --git a/calc.go b/calc.go index 78846d23a1..d8161cef01 100644 --- a/calc.go +++ b/calc.go @@ -1419,7 +1419,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (arg fo err = newInvalidColumnNameError(tokens[1]) return } - cr.Col = TotalColumns + cr.Col = MaxColumns } } if refs.Len() > 0 { @@ -1439,7 +1439,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (arg fo err = newInvalidColumnNameError(tokens[0]) return } - cr.Col = TotalColumns + cr.Col = MaxColumns } cellRanges.PushBack(cellRange{ From: cellRef{Sheet: sheet, Col: cr.Col, Row: 1}, @@ -14457,8 +14457,8 @@ func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "COLUMNS requires 1 argument") } min, max := calcColumnsMinMax(argsList) - if max == TotalColumns { - return newNumberFormulaArg(float64(TotalColumns)) + if max == MaxColumns { + return newNumberFormulaArg(float64(MaxColumns)) } result := max - min + 1 if max == min { diff --git a/errors.go b/errors.go index 6606f1eaf5..f5ea06e658 100644 --- a/errors.go +++ b/errors.go @@ -61,7 +61,7 @@ func newInvalidStyleID(styleID int) error { // newFieldLengthError defined the error message on receiving the field length // overflow. func newFieldLengthError(name string) error { - return fmt.Errorf("field %s must be less or equal than 255 characters", name) + return fmt.Errorf("field %s must be less than or equal to 255 characters", name) } // newCellNameToCoordinatesError defined the error message on converts @@ -76,10 +76,10 @@ var ( ErrStreamSetColWidth = errors.New("must call the SetColWidth function before the SetRow function") // ErrColumnNumber defined the error message on receive an invalid column // number. - ErrColumnNumber = errors.New("column number exceeds maximum limit") + ErrColumnNumber = fmt.Errorf(`the column number must be greater than or equal to %d and less than or equal to %d`, MinColumns, MaxColumns) // ErrColumnWidth defined the error message on receive an invalid column // width. - ErrColumnWidth = fmt.Errorf("the width of the column must be smaller than or equal to %d characters", MaxColumnWidth) + ErrColumnWidth = fmt.Errorf("the width of the column must be less than or equal to %d characters", MaxColumnWidth) // ErrOutlineLevel defined the error message on receive an invalid outline // level number. ErrOutlineLevel = errors.New("invalid outline level") @@ -102,7 +102,7 @@ var ( ErrMaxRows = errors.New("row number exceeds maximum limit") // ErrMaxRowHeight defined the error message on receive an invalid row // height. - ErrMaxRowHeight = fmt.Errorf("the height of the row must be smaller than or equal to %d points", MaxRowHeight) + ErrMaxRowHeight = fmt.Errorf("the height of the row must be less than or equal to %d points", MaxRowHeight) // ErrImgExt defined the error message on receive an unsupported image // extension. ErrImgExt = errors.New("unsupported image extension") @@ -143,7 +143,7 @@ var ( ErrCustomNumFmt = errors.New("custom number format can not be empty") // ErrFontLength defined the error message on the length of the font // family name overflow. - ErrFontLength = fmt.Errorf("the length of the font family name must be smaller than or equal to %d", MaxFontFamilyLength) + ErrFontLength = fmt.Errorf("the length of the font family name must be less than or equal to %d", MaxFontFamilyLength) // ErrFontSize defined the error message on the size of the font is invalid. ErrFontSize = fmt.Errorf("font size must be between %d and %d points", MinFontSize, MaxFontSize) // ErrSheetIdx defined the error message on receive the invalid worksheet diff --git a/lib.go b/lib.go index f285a40dbc..3170a6d6de 100644 --- a/lib.go +++ b/lib.go @@ -211,7 +211,7 @@ func ColumnNameToNumber(name string) (int, error) { } multi *= 26 } - if col > TotalColumns { + if col > MaxColumns { return -1, ErrColumnNumber } return col, nil @@ -225,10 +225,7 @@ func ColumnNameToNumber(name string) (int, error) { // excelize.ColumnNumberToName(37) // returns "AK", nil // func ColumnNumberToName(num int) (string, error) { - if num < 1 { - return "", fmt.Errorf("incorrect column number %d", num) - } - if num > TotalColumns { + if num < MinColumns || num > MaxColumns { return "", ErrColumnNumber } var col string diff --git a/lib_test.go b/lib_test.go index 027e5dd6b6..64acb8ae61 100644 --- a/lib_test.go +++ b/lib_test.go @@ -79,7 +79,7 @@ func TestColumnNameToNumber_Error(t *testing.T) { } } _, err := ColumnNameToNumber("XFE") - assert.EqualError(t, err, ErrColumnNumber.Error()) + assert.ErrorIs(t, err, ErrColumnNumber) } func TestColumnNumberToName_OK(t *testing.T) { @@ -103,8 +103,8 @@ func TestColumnNumberToName_Error(t *testing.T) { assert.Equal(t, "", out) } - _, err = ColumnNumberToName(TotalColumns + 1) - assert.EqualError(t, err, ErrColumnNumber.Error()) + _, err = ColumnNumberToName(MaxColumns + 1) + assert.ErrorIs(t, err, ErrColumnNumber) } func TestSplitCellName_OK(t *testing.T) { diff --git a/sheet.go b/sheet.go index e88f9536e5..2a722c9858 100644 --- a/sheet.go +++ b/sheet.go @@ -256,7 +256,7 @@ func replaceRelationshipsBytes(content []byte) []byte { // SetActiveSheet provides a function to set the default active sheet of the // workbook by a given index. Note that the active index is different from the -// ID returned by function GetSheetMap(). It should be greater or equal to 0 +// ID returned by function GetSheetMap(). It should be greater than or equal to 0 // and less than the total worksheet numbers. func (f *File) SetActiveSheet(index int) { if index < 0 { diff --git a/stream.go b/stream.go index 641340ede9..0db2438888 100644 --- a/stream.go +++ b/stream.go @@ -387,10 +387,7 @@ func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { if sw.sheetWritten { return ErrStreamSetColWidth } - if min > TotalColumns || max > TotalColumns { - return ErrColumnNumber - } - if min < 1 || max < 1 { + if min < MinColumns || min > MaxColumns || max < MinColumns || max > MaxColumns { return ErrColumnNumber } if width > MaxColumnWidth { diff --git a/stream_test.go b/stream_test.go index 9776b384a9..6843e2064f 100644 --- a/stream_test.go +++ b/stream_test.go @@ -75,7 +75,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) // Test set cell column overflow. - assert.EqualError(t, streamWriter.SetRow("XFD1", []interface{}{"A", "B", "C"}), ErrColumnNumber.Error()) + assert.ErrorIs(t, streamWriter.SetRow("XFD1", []interface{}{"A", "B", "C"}), ErrColumnNumber) // Test close temporary file error. file = NewFile() @@ -139,8 +139,8 @@ func TestStreamSetColWidth(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.SetColWidth(3, 2, 20)) - assert.EqualError(t, streamWriter.SetColWidth(0, 3, 20), ErrColumnNumber.Error()) - assert.EqualError(t, streamWriter.SetColWidth(TotalColumns+1, 3, 20), ErrColumnNumber.Error()) + assert.ErrorIs(t, streamWriter.SetColWidth(0, 3, 20), ErrColumnNumber) + assert.ErrorIs(t, streamWriter.SetColWidth(MaxColumns+1, 3, 20), ErrColumnNumber) assert.EqualError(t, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1), ErrColumnWidth.Error()) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) assert.EqualError(t, streamWriter.SetColWidth(2, 3, 20), ErrStreamSetColWidth.Error()) diff --git a/xmlDrawing.go b/xmlDrawing.go index 480868593a..3e54b7207f 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -109,7 +109,8 @@ const ( MaxRowHeight = 409 MinFontSize = 1 TotalRows = 1048576 - TotalColumns = 16384 + MinColumns = 1 + MaxColumns = 16384 TotalSheetHyperlinks = 65529 TotalCellChars = 32767 // pivotTableVersion should be greater than 3. One or more of the From 40ed1d1b81b4d30165bb73e9406e59ddbdb76fe2 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 16 Jul 2022 12:50:13 +0800 Subject: [PATCH 077/213] Fix potential file corrupted when changing cell value or the col/row - Remove shared formula subsequent cell when setting the cell values - Support adjust table range when removing and inserting column/row --- adjust.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++--- adjust_test.go | 38 +++++++++++++++++++++++++--- cell.go | 30 ++++++++++++++++++---- picture_test.go | 2 +- rows_test.go | 2 +- sheet.go | 11 ++++---- table.go | 49 ++++++++++++++++++++---------------- table_test.go | 8 +++++- 8 files changed, 165 insertions(+), 42 deletions(-) diff --git a/adjust.go b/adjust.go index e1c0e15e69..d766b3ebf8 100644 --- a/adjust.go +++ b/adjust.go @@ -11,6 +11,13 @@ package excelize +import ( + "bytes" + "encoding/xml" + "io" + "strings" +) + type adjustDirection bool const ( @@ -41,6 +48,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) f.adjustColDimensions(ws, num, offset) } f.adjustHyperlinks(ws, sheet, dir, num, offset) + f.adjustTable(ws, sheet, dir, num, offset) if err = f.adjustMergeCells(ws, dir, num, offset); err != nil { return err } @@ -138,6 +146,54 @@ func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirec } } +// adjustTable provides a function to update the table when inserting or +// deleting rows or columns. +func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) { + if ws.TableParts == nil || len(ws.TableParts.TableParts) == 0 { + return + } + for idx := 0; idx < len(ws.TableParts.TableParts); idx++ { + tbl := ws.TableParts.TableParts[idx] + target := f.getSheetRelationshipsTargetByID(sheet, tbl.RID) + tableXML := strings.ReplaceAll(target, "..", "xl") + content, ok := f.Pkg.Load(tableXML) + if !ok { + continue + } + t := xlsxTable{} + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + Decode(&t); err != nil && err != io.EOF { + return + } + coordinates, err := areaRefToCoordinates(t.Ref) + if err != nil { + return + } + // Remove the table when deleting the header row of the table + if dir == rows && num == coordinates[0] { + ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...) + ws.TableParts.Count = len(ws.TableParts.TableParts) + idx-- + continue + } + coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset) + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] + if y2-y1 < 2 || x2-x1 < 1 { + ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...) + ws.TableParts.Count = len(ws.TableParts.TableParts) + idx-- + continue + } + t.Ref, _ = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}) + if t.AutoFilter != nil { + t.AutoFilter.Ref = t.Ref + } + _, _ = f.setTableHeader(sheet, x1, y1, x2) + table, _ := xml.Marshal(t) + f.saveFileList(tableXML, table) + } +} + // adjustAutoFilter provides a function to update the auto filter when // inserting or deleting rows or columns. func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, offset int) error { @@ -182,10 +238,13 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu if coordinates[3] >= num { coordinates[3] += offset } - } else { - if coordinates[2] >= num { - coordinates[2] += offset - } + return coordinates + } + if coordinates[0] >= num { + coordinates[0] += offset + } + if coordinates[2] >= num { + coordinates[2] += offset } return coordinates } diff --git a/adjust_test.go b/adjust_test.go index ab6bedcc06..1d807054e4 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -1,6 +1,8 @@ package excelize import ( + "fmt" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -281,7 +283,7 @@ func TestAdjustAutoFilter(t *testing.T) { Ref: "A1:A3", }, }, rows, 1, -1)) - // testing adjustAutoFilter with illegal cell coordinates. + // Test adjustAutoFilter with illegal cell coordinates. assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{ Ref: "A:B1", @@ -294,6 +296,36 @@ func TestAdjustAutoFilter(t *testing.T) { }, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) } +func TestAdjustTable(t *testing.T) { + f, sheetName := NewFile(), "Sheet1" + for idx, tableRange := range [][]string{{"B2", "C3"}, {"E3", "F5"}, {"H5", "H8"}, {"J5", "K9"}} { + assert.NoError(t, f.AddTable(sheetName, tableRange[0], tableRange[1], fmt.Sprintf(`{ + "table_name": "table%d", + "table_style": "TableStyleMedium2", + "show_first_column": true, + "show_last_column": true, + "show_row_stripes": false, + "show_column_stripes": true + }`, idx))) + } + assert.NoError(t, f.RemoveRow(sheetName, 2)) + assert.NoError(t, f.RemoveRow(sheetName, 3)) + assert.NoError(t, f.RemoveCol(sheetName, "H")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustTable.xlsx"))) + + f = NewFile() + assert.NoError(t, f.AddTable(sheetName, "A1", "D5", "")) + // Test adjust table with non-table part + f.Pkg.Delete("xl/tables/table1.xml") + assert.NoError(t, f.RemoveRow(sheetName, 1)) + // Test adjust table with unsupported charset + f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) + assert.NoError(t, f.RemoveRow(sheetName, 1)) + // Test adjust table with invalid table range reference + f.Pkg.Store("xl/tables/table1.xml", []byte(``)) + assert.NoError(t, f.RemoveRow(sheetName, 1)) +} + func TestAdjustHelper(t *testing.T) { f := NewFile() f.NewSheet("Sheet2") @@ -303,10 +335,10 @@ func TestAdjustHelper(t *testing.T) { f.Sheet.Store("xl/worksheets/sheet2.xml", &xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{Ref: "A1:B"}, }) - // testing adjustHelper with illegal cell coordinates. + // Test adjustHelper with illegal cell coordinates. assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) - // testing adjustHelper on not exists worksheet. + // Test adjustHelper on not exists worksheet. assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN is not exist") } diff --git a/cell.go b/cell.go index 286085b3ab..97425c5795 100644 --- a/cell.go +++ b/cell.go @@ -169,6 +169,21 @@ func (c *xlsxC) hasValue() bool { return c.S != 0 || c.V != "" || c.F != nil || c.T != "" } +// removeFormula delete formula for the cell. +func (c *xlsxC) removeFormula(ws *xlsxWorksheet) { + if c.F != nil && c.F.T == STCellFormulaTypeShared && c.F.Ref != "" { + si := c.F.Si + for r, row := range ws.SheetData.Row { + for col, cell := range row.C { + if cell.F != nil && cell.F.Si != nil && *cell.F.Si == *si { + ws.SheetData.Row[r].C[col].F = nil + } + } + } + } + c.F = nil +} + // setCellIntFunc is a wrapper of SetCellInt. func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error { var err error @@ -266,7 +281,8 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellInt(value) - cellData.F, cellData.IS = nil, nil + cellData.removeFormula(ws) + cellData.IS = nil return err } @@ -292,7 +308,8 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellBool(value) - cellData.F, cellData.IS = nil, nil + cellData.removeFormula(ws) + cellData.IS = nil return err } @@ -330,7 +347,8 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, precision, bitSiz defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellFloat(value, precision, bitSize) - cellData.F, cellData.IS = nil, nil + cellData.removeFormula(ws) + cellData.IS = nil return err } @@ -356,7 +374,8 @@ func (f *File) SetCellStr(sheet, axis, value string) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V, err = f.setCellString(value) - cellData.F, cellData.IS = nil, nil + cellData.removeFormula(ws) + cellData.IS = nil return err } @@ -455,7 +474,8 @@ func (f *File) SetCellDefault(sheet, axis, value string) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellDefault(value) - cellData.F, cellData.IS = nil, nil + cellData.removeFormula(ws) + cellData.IS = nil return err } diff --git a/picture_test.go b/picture_test.go index 60c6ac17cc..3ac1afb69c 100644 --- a/picture_test.go +++ b/picture_test.go @@ -173,7 +173,7 @@ func TestGetPicture(t *testing.T) { } func TestAddDrawingPicture(t *testing.T) { - // testing addDrawingPicture with illegal cell coordinates. + // Test addDrawingPicture with illegal cell coordinates. f := NewFile() assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, 0, 0, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } diff --git a/rows_test.go b/rows_test.go index 014b2d853f..4fe28517cd 100644 --- a/rows_test.go +++ b/rows_test.go @@ -322,7 +322,7 @@ func TestInsertRow(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRow.xlsx"))) } -// Testing internal structure state after insert operations. It is important +// Test internal structure state after insert operations. It is important // for insert workflow to be constant to avoid side effect with functions // related to internal structure. func TestInsertRowInEmptyFile(t *testing.T) { diff --git a/sheet.go b/sheet.go index 2a722c9858..6dda8118be 100644 --- a/sheet.go +++ b/sheet.go @@ -478,12 +478,11 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } // DeleteSheet provides a function to delete worksheet in a workbook by given -// worksheet name, the sheet names are not case-sensitive. The sheet names are -// not case-sensitive. Use this method with caution, which will affect -// changes in references such as formulas, charts, and so on. If there is any -// referenced value of the deleted worksheet, it will cause a file error when -// you open it. This function will be invalid when only the one worksheet is -// left. +// worksheet name, the sheet names are not case-sensitive. Use this method +// with caution, which will affect changes in references such as formulas, +// charts, and so on. If there is any referenced value of the deleted +// worksheet, it will cause a file error when you open it. This function will +// be invalid when only one worksheet is left func (f *File) DeleteSheet(name string) { if f.SheetCount == 1 || f.GetSheetIndex(name) == -1 { return diff --git a/table.go b/table.go index 413118c31a..84445b79f1 100644 --- a/table.go +++ b/table.go @@ -129,28 +129,18 @@ func (f *File) addSheetTable(sheet string, rID int) error { return err } -// addTable provides a function to add table by given worksheet name, -// coordinate area and format set. -func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet *formatTable) error { - // Correct the minimum number of rows, the table at least two lines. - if y1 == y2 { - y2++ - } - - // Correct table reference coordinate area, such correct C1:B3 to B1:C3. - ref, err := f.coordinatesToAreaRef([]int{x1, y1, x2, y2}) - if err != nil { - return err - } - - var tableColumn []*xlsxTableColumn - - idx := 0 +// setTableHeader provides a function to set cells value in header row for the +// table. +func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn, error) { + var ( + tableColumns []*xlsxTableColumn + idx int + ) for i := x1; i <= x2; i++ { idx++ cell, err := CoordinatesToCellName(i, y1) if err != nil { - return err + return tableColumns, err } name, _ := f.GetCellValue(sheet, cell) if _, err := strconv.Atoi(name); err == nil { @@ -160,11 +150,28 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet name = "Column" + strconv.Itoa(idx) _ = f.SetCellStr(sheet, cell, name) } - tableColumn = append(tableColumn, &xlsxTableColumn{ + tableColumns = append(tableColumns, &xlsxTableColumn{ ID: idx, Name: name, }) } + return tableColumns, nil +} + +// addTable provides a function to add table by given worksheet name, +// coordinate area and format set. +func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet *formatTable) error { + // Correct the minimum number of rows, the table at least two lines. + if y1 == y2 { + y2++ + } + + // Correct table reference coordinate area, such correct C1:B3 to B1:C3. + ref, err := f.coordinatesToAreaRef([]int{x1, y1, x2, y2}) + if err != nil { + return err + } + tableColumns, _ := f.setTableHeader(sheet, x1, y1, x2) name := formatSet.TableName if name == "" { name = "Table" + strconv.Itoa(i) @@ -179,8 +186,8 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet Ref: ref, }, TableColumns: &xlsxTableColumns{ - Count: idx, - TableColumn: tableColumn, + Count: len(tableColumns), + TableColumn: tableColumns, }, TableStyleInfo: &xlsxTableStyleInfo{ Name: formatSet.TableStyle, diff --git a/table_test.go b/table_test.go index 0a74b1b568..5941c508ab 100644 --- a/table_test.go +++ b/table_test.go @@ -45,6 +45,12 @@ func TestAddTable(t *testing.T) { assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell coordinates [0, 0]") } +func TestSetTableHeader(t *testing.T) { + f := NewFile() + _, err := f.setTableHeader("Sheet1", 1, 0, 1) + assert.EqualError(t, err, "invalid cell coordinates [1, 0]") +} + func TestAutoFilter(t *testing.T) { outFile := filepath.Join("test", "TestAutoFilter%d.xlsx") @@ -72,7 +78,7 @@ func TestAutoFilter(t *testing.T) { }) } - // testing AutoFilter with illegal cell coordinates. + // Test AutoFilter with illegal cell coordinates. assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) } From 0d4c97c88aa9254a4db5a0b9192d0f431ff90e43 Mon Sep 17 00:00:00 2001 From: Regan Yue <1131625869@qq.com> Date: Sun, 17 Jul 2022 12:18:25 +0800 Subject: [PATCH 078/213] Optimizing line breaks for comments (#1281) --- file.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/file.go b/file.go index 5931bdb4fb..ce8b138336 100644 --- a/file.go +++ b/file.go @@ -21,8 +21,8 @@ import ( "sync" ) -// NewFile provides a function to create new file by default template. For -// example: +// NewFile provides a function to create new file by default template. +// For example: // // f := NewFile() // From ebea684ae5c60776d4d8364b7360d0c0603cb3b0 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 18 Jul 2022 00:21:34 +0800 Subject: [PATCH 079/213] Fix potential file corrupted and change worksheet name case-insensitive - Using sheet ID instead of sheet index when delete the cell in calculation chain - Update documentation for exported functions - Using `sheet` represent the sheet name in the function parameters --- calcchain.go | 2 +- cell.go | 55 +++++++++++++----------- col.go | 12 +++++- comment.go | 3 +- drawing.go | 6 ++- excelize.go | 2 +- lib.go | 6 +-- picture.go | 4 +- pivotTable.go | 2 +- rows.go | 19 ++++---- shape.go | 3 +- sheet.go | 112 +++++++++++++++++++++++++++--------------------- sheetpr.go | 8 ++-- sheetview.go | 8 ++-- stream.go | 4 +- table.go | 3 +- xmlWorksheet.go | 9 ++-- 17 files changed, 148 insertions(+), 110 deletions(-) diff --git a/calcchain.go b/calcchain.go index a1f9c0c56a..1007de145a 100644 --- a/calcchain.go +++ b/calcchain.go @@ -49,7 +49,7 @@ func (f *File) deleteCalcChain(index int, axis string) { calc := f.calcChainReader() if calc != nil { calc.C = xlsxCalcChainCollection(calc.C).Filter(func(c xlsxCalcChainC) bool { - return !((c.I == index && c.R == axis) || (c.I == index && axis == "")) + return !((c.I == index && c.R == axis) || (c.I == index && axis == "") || (c.I == 0 && c.R == axis)) }) } if len(calc.C) == 0 { diff --git a/cell.go b/cell.go index 97425c5795..5506189118 100644 --- a/cell.go +++ b/cell.go @@ -170,18 +170,21 @@ func (c *xlsxC) hasValue() bool { } // removeFormula delete formula for the cell. -func (c *xlsxC) removeFormula(ws *xlsxWorksheet) { - if c.F != nil && c.F.T == STCellFormulaTypeShared && c.F.Ref != "" { - si := c.F.Si - for r, row := range ws.SheetData.Row { - for col, cell := range row.C { - if cell.F != nil && cell.F.Si != nil && *cell.F.Si == *si { - ws.SheetData.Row[r].C[col].F = nil +func (f *File) removeFormula(c *xlsxC, ws *xlsxWorksheet, sheet string) { + if c.F != nil && c.Vm == nil { + f.deleteCalcChain(f.getSheetID(sheet), c.R) + if c.F.T == STCellFormulaTypeShared && c.F.Ref != "" { + si := c.F.Si + for r, row := range ws.SheetData.Row { + for col, cell := range row.C { + if cell.F != nil && cell.F.Si != nil && *cell.F.Si == *si { + ws.SheetData.Row[r].C[col].F = nil + } } } } + c.F = nil } - c.F = nil } // setCellIntFunc is a wrapper of SetCellInt. @@ -281,8 +284,8 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellInt(value) - cellData.removeFormula(ws) cellData.IS = nil + f.removeFormula(cellData, ws, sheet) return err } @@ -308,8 +311,8 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellBool(value) - cellData.removeFormula(ws) cellData.IS = nil + f.removeFormula(cellData, ws, sheet) return err } @@ -347,8 +350,8 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, precision, bitSiz defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellFloat(value, precision, bitSize) - cellData.removeFormula(ws) cellData.IS = nil + f.removeFormula(cellData, ws, sheet) return err } @@ -374,8 +377,8 @@ func (f *File) SetCellStr(sheet, axis, value string) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V, err = f.setCellString(value) - cellData.removeFormula(ws) cellData.IS = nil + f.removeFormula(cellData, ws, sheet) return err } @@ -474,8 +477,8 @@ func (f *File) SetCellDefault(sheet, axis, value string) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellDefault(value) - cellData.removeFormula(ws) cellData.IS = nil + f.removeFormula(cellData, ws, sheet) return err } @@ -510,13 +513,12 @@ type FormulaOpts struct { } // SetCellFormula provides a function to set formula on the cell is taken -// according to the given worksheet name (case-sensitive) and cell formula -// settings. The result of the formula cell can be calculated when the -// worksheet is opened by the Office Excel application or can be using -// the "CalcCellValue" function also can get the calculated cell value. If -// the Excel application doesn't calculate the formula automatically when the -// workbook has been opened, please call "UpdateLinkedValue" after setting -// the cell formula functions. +// according to the given worksheet name and cell formula settings. The result +// of the formula cell can be calculated when the worksheet is opened by the +// Office Excel application or can be using the "CalcCellValue" function also +// can get the calculated cell value. If the Excel application doesn't +// calculate the formula automatically when the workbook has been opened, +// please call "UpdateLinkedValue" after setting the cell formula functions. // // Example 1, set normal formula "=SUM(A1,B1)" for the cell "A3" on "Sheet1": // @@ -662,11 +664,12 @@ func (ws *xlsxWorksheet) countSharedFormula() (count int) { return } -// GetCellHyperLink provides a function to get cell hyperlink by given -// worksheet name and axis. Boolean type value link will be true if the cell -// has a hyperlink and the target is the address of the hyperlink. Otherwise, -// the value of link will be false and the value of the target will be a blank -// string. For example get hyperlink of Sheet1!H6: +// GetCellHyperLink gets a cell hyperlink based on the given worksheet name and +// cell coordinates. If the cell has a hyperlink, it will return 'true' and +// the link address, otherwise it will return 'false' and an empty link +// address. +// +// For example, get a hyperlink to a 'H6' cell on a worksheet named 'Sheet1': // // link, target, err := f.GetCellHyperLink("Sheet1", "H6") // @@ -765,7 +768,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...Hype switch linkType { case "External": - sheetPath := f.sheetMap[trimSheetName(sheet)] + sheetPath, _ := f.getSheetXMLPath(sheet) sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" rID := f.setRels(linkData.RID, sheetRels, SourceRelationshipHyperLink, link, linkType) linkData = xlsxHyperlink{ diff --git a/col.go b/col.go index ee1a4074d5..95c7961d35 100644 --- a/col.go +++ b/col.go @@ -40,7 +40,15 @@ type Cols struct { sheetXML []byte } -// GetCols return all the columns in a sheet by given worksheet name (case-sensitive). For example: +// GetCols gets the value of all cells by columns on the worksheet based on the +// given worksheet name, returned as a two-dimensional array, where the value +// of the cell is converted to the `string` type. If the cell format can be +// applied to the value of the cell, the applied value will be used, otherwise +// the original value will be used. +// +// For example, get and traverse the value of all cells by columns on a +// worksheet named +// 'Sheet1': // // cols, err := f.GetCols("Sheet1") // if err != nil { @@ -196,7 +204,7 @@ func columnXMLHandler(colIterator *columnXMLIterator, xmlElement *xml.StartEleme // } // func (f *File) Cols(sheet string) (*Cols, error) { - name, ok := f.sheetMap[trimSheetName(sheet)] + name, ok := f.getSheetXMLPath(sheet) if !ok { return nil, ErrSheetNotExist{sheet} } diff --git a/comment.go b/comment.go index 0e3945dfa2..23f1079ad0 100644 --- a/comment.go +++ b/comment.go @@ -115,7 +115,8 @@ func (f *File) AddComment(sheet, cell, format string) error { drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") } else { // Add first comment for given sheet. - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" + sheetXMLPath, _ := f.getSheetXMLPath(sheet) + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") f.addSheetNameSpace(sheet, SourceRelationship) diff --git a/drawing.go b/drawing.go index 7de0fb9136..10f4cd0f2c 100644 --- a/drawing.go +++ b/drawing.go @@ -33,7 +33,8 @@ func (f *File) prepareDrawing(ws *xlsxWorksheet, drawingID int, sheet, drawingXM drawingXML = strings.ReplaceAll(sheetRelationshipsDrawingXML, "..", "xl") } else { // Add first picture for given sheet. - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" + sheetXMLPath, _ := f.getSheetXMLPath(sheet) + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) } @@ -45,7 +46,8 @@ func (f *File) prepareDrawing(ws *xlsxWorksheet, drawingID int, sheet, drawingXM func (f *File) prepareChartSheetDrawing(cs *xlsxChartsheet, drawingID int, sheet string) { sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" // Only allow one chart in a chartsheet. - sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/chartsheets/") + ".rels" + sheetXMLPath, _ := f.getSheetXMLPath(sheet) + sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/chartsheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetNameSpace(sheet, SourceRelationship) cs.Drawing = &xlsxDrawing{ diff --git a/excelize.go b/excelize.go index da51b13c81..6603db097d 100644 --- a/excelize.go +++ b/excelize.go @@ -230,7 +230,7 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { name string ok bool ) - if name, ok = f.sheetMap[trimSheetName(sheet)]; !ok { + if name, ok = f.getSheetXMLPath(sheet); !ok { err = fmt.Errorf("sheet %s is not exist", sheet) return } diff --git a/lib.go b/lib.go index 3170a6d6de..99118ff079 100644 --- a/lib.go +++ b/lib.go @@ -187,8 +187,8 @@ func JoinCellName(col string, row int) (string, error) { } // ColumnNameToNumber provides a function to convert Excel sheet column name -// to int. Column name case-insensitive. The function returns an error if -// column name incorrect. +// (case-insensitive) to int. The function returns an error if column name +// incorrect. // // Example: // @@ -690,7 +690,7 @@ func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) { // addSheetNameSpace add XML attribute for worksheet. func (f *File) addSheetNameSpace(sheet string, ns xml.Attr) { - name := f.sheetMap[trimSheetName(sheet)] + name, _ := f.getSheetXMLPath(sheet) f.addNameSpaces(name, ns) } diff --git a/picture.go b/picture.go index 44f1f3b3e8..c3d0df7050 100644 --- a/picture.go +++ b/picture.go @@ -200,7 +200,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, // xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and // relationship index. func (f *File) deleteSheetRelationships(sheet, rID string) { - name, ok := f.sheetMap[trimSheetName(sheet)] + name, ok := f.getSheetXMLPath(sheet) if !ok { name = strings.ToLower(sheet) + ".xml" } @@ -450,7 +450,7 @@ func (f *File) addContentTypePart(index int, contentType string) { // value in xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and // relationship index. func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { - name, ok := f.sheetMap[trimSheetName(sheet)] + name, ok := f.getSheetXMLPath(sheet) if !ok { name = strings.ToLower(sheet) + ".xml" } diff --git a/pivotTable.go b/pivotTable.go index 10c48cef3a..73e5d34cfa 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -190,7 +190,7 @@ func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet, if err != nil { return dataSheet, "", err } - pivotTableSheetPath, ok := f.sheetMap[trimSheetName(pivotTableSheetName)] + pivotTableSheetPath, ok := f.getSheetXMLPath(pivotTableSheetName) if !ok { return dataSheet, pivotTableSheetPath, fmt.Errorf("sheet %s is not exist", pivotTableSheetName) } diff --git a/rows.go b/rows.go index f83d425382..853c8f7df3 100644 --- a/rows.go +++ b/rows.go @@ -26,13 +26,16 @@ import ( "github.com/mohae/deepcopy" ) -// GetRows return all the rows in a sheet by given worksheet name -// (case sensitive), returned as a two-dimensional array, where the value of -// the cell is converted to the string type. If the cell format can be applied -// to the value of the cell, the applied value will be used, otherwise the -// original value will be used. GetRows fetched the rows with value or formula -// cells, the continually blank cells in the tail of each row will be skipped, -// so the length of each row may be inconsistent. For example: +// GetRows return all the rows in a sheet by given worksheet name, returned as +// a two-dimensional array, where the value of the cell is converted to the +// string type. If the cell format can be applied to the value of the cell, +// the applied value will be used, otherwise the original value will be used. +// GetRows fetched the rows with value or formula cells, the continually blank +// cells in the tail of each row will be skipped, so the length of each row +// may be inconsistent. +// +// For example, get and traverse the value of all cells by rows on a worksheet +// named 'Sheet1': // // rows, err := f.GetRows("Sheet1") // if err != nil { @@ -233,7 +236,7 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta // } // func (f *File) Rows(sheet string) (*Rows, error) { - name, ok := f.sheetMap[trimSheetName(sheet)] + name, ok := f.getSheetXMLPath(sheet) if !ok { return nil, ErrSheetNotExist{sheet} } diff --git a/shape.go b/shape.go index ddf9e317c6..58751b25b9 100644 --- a/shape.go +++ b/shape.go @@ -300,7 +300,8 @@ func (f *File) AddShape(sheet, cell, format string) error { drawingXML = strings.ReplaceAll(sheetRelationshipsDrawingXML, "..", "xl") } else { // Add first shape for given sheet. - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" + sheetXMLPath, _ := f.getSheetXMLPath(sheet) + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) f.addSheetNameSpace(sheet, SourceRelationship) diff --git a/sheet.go b/sheet.go index 6dda8118be..51410b043f 100644 --- a/sheet.go +++ b/sheet.go @@ -34,17 +34,16 @@ import ( ) // NewSheet provides the function to create a new sheet by given a worksheet -// name and returns the index of the sheets in the workbook -// (spreadsheet) after it appended. Note that the worksheet names are not -// case-sensitive, when creating a new spreadsheet file, the default -// worksheet named `Sheet1` will be created. -func (f *File) NewSheet(name string) int { +// name and returns the index of the sheets in the workbook after it appended. +// Note that when creating a new workbook, the default worksheet named +// `Sheet1` will be created. +func (f *File) NewSheet(sheet string) int { // Check if the worksheet already exists - index := f.GetSheetIndex(name) + index := f.GetSheetIndex(sheet) if index != -1 { return index } - f.DeleteSheet(name) + f.DeleteSheet(sheet) f.SheetCount++ wb := f.workbookReader() sheetID := 0 @@ -59,12 +58,12 @@ func (f *File) NewSheet(name string) int { // Update [Content_Types].xml f.setContentTypes("/xl/worksheets/sheet"+strconv.Itoa(sheetID)+".xml", ContentTypeSpreadSheetMLWorksheet) // Create new sheet /xl/worksheets/sheet%d.xml - f.setSheet(sheetID, name) + f.setSheet(sheetID, sheet) // Update workbook.xml.rels rID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipWorkSheet, fmt.Sprintf("/xl/worksheets/sheet%d.xml", sheetID), "") // Update workbook.xml - f.setWorkbook(name, sheetID, rID) - return f.GetSheetIndex(name) + f.setWorkbook(sheet, sheetID, rID) + return f.GetSheetIndex(sheet) } // contentTypesReader provides a function to get the pointer to the @@ -345,7 +344,7 @@ func (f *File) getActiveSheetID() int { func (f *File) SetSheetName(oldName, newName string) { oldName = trimSheetName(oldName) newName = trimSheetName(newName) - if newName == oldName { + if strings.EqualFold(newName, oldName) { return } content := f.workbookReader() @@ -374,9 +373,10 @@ func (f *File) GetSheetName(index int) (name string) { // getSheetID provides a function to get worksheet ID of the spreadsheet by // given sheet name. If given worksheet name is invalid, will return an // integer type value -1. -func (f *File) getSheetID(name string) int { - for sheetID, sheet := range f.GetSheetMap() { - if strings.EqualFold(sheet, trimSheetName(name)) { +func (f *File) getSheetID(sheet string) int { + sheetName := trimSheetName(sheet) + for sheetID, name := range f.GetSheetMap() { + if strings.EqualFold(name, sheetName) { return sheetID } } @@ -384,12 +384,12 @@ func (f *File) getSheetID(name string) int { } // GetSheetIndex provides a function to get a sheet index of the workbook by -// the given sheet name, the sheet names are not case-sensitive. If the given -// sheet name is invalid or sheet doesn't exist, it will return an integer -// type value -1. -func (f *File) GetSheetIndex(name string) int { - for index, sheet := range f.GetSheetList() { - if strings.EqualFold(sheet, trimSheetName(name)) { +// the given sheet name. If the given sheet name is invalid or sheet doesn't +// exist, it will return an integer type value -1. +func (f *File) GetSheetIndex(sheet string) int { + sheetName := trimSheetName(sheet) + for index, name := range f.GetSheetList() { + if strings.EqualFold(name, sheetName) { return index } } @@ -455,6 +455,22 @@ func (f *File) getSheetMap() map[string]string { return maps } +// getSheetXMLPath provides a function to get XML file path by given sheet +// name. +func (f *File) getSheetXMLPath(sheet string) (string, bool) { + var ( + name string + ok bool + ) + for sheetName, filePath := range f.sheetMap { + if strings.EqualFold(sheetName, sheet) { + name, ok = filePath, true + break + } + } + return name, ok +} + // SetSheetBackground provides a function to set background picture by given // worksheet name and file path. func (f *File) SetSheetBackground(sheet, picture string) error { @@ -469,7 +485,8 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } file, _ := ioutil.ReadFile(filepath.Clean(picture)) name := f.addMedia(file, ext) - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" + sheetXMLPath, _ := f.getSheetXMLPath(sheet) + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") f.addSheetPicture(sheet, rID) f.addSheetNameSpace(sheet, SourceRelationship) @@ -478,24 +495,23 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } // DeleteSheet provides a function to delete worksheet in a workbook by given -// worksheet name, the sheet names are not case-sensitive. Use this method -// with caution, which will affect changes in references such as formulas, -// charts, and so on. If there is any referenced value of the deleted -// worksheet, it will cause a file error when you open it. This function will -// be invalid when only one worksheet is left -func (f *File) DeleteSheet(name string) { - if f.SheetCount == 1 || f.GetSheetIndex(name) == -1 { +// worksheet name. Use this method with caution, which will affect changes in +// references such as formulas, charts, and so on. If there is any referenced +// value of the deleted worksheet, it will cause a file error when you open +// it. This function will be invalid when only one worksheet is left. +func (f *File) DeleteSheet(sheet string) { + if f.SheetCount == 1 || f.GetSheetIndex(sheet) == -1 { return } - sheetName := trimSheetName(name) + sheetName := trimSheetName(sheet) wb := f.workbookReader() wbRels := f.relsReader(f.getWorkbookRelsPath()) activeSheetName := f.GetSheetName(f.GetActiveSheetIndex()) - deleteLocalSheetID := f.GetSheetIndex(name) + deleteLocalSheetID := f.GetSheetIndex(sheet) deleteAndAdjustDefinedNames(wb, deleteLocalSheetID) - for idx, sheet := range wb.Sheets.Sheet { - if !strings.EqualFold(sheet.Name, sheetName) { + for idx, v := range wb.Sheets.Sheet { + if !strings.EqualFold(v.Name, sheetName) { continue } @@ -503,16 +519,17 @@ func (f *File) DeleteSheet(name string) { var sheetXML, rels string if wbRels != nil { for _, rel := range wbRels.Relationships { - if rel.ID == sheet.ID { + if rel.ID == v.ID { sheetXML = f.getWorksheetPath(rel.Target) - rels = "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[sheetName], "xl/worksheets/") + ".rels" + sheetXMLPath, _ := f.getSheetXMLPath(sheet) + rels = "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" } } } - target := f.deleteSheetFromWorkbookRels(sheet.ID) + target := f.deleteSheetFromWorkbookRels(v.ID) f.deleteSheetFromContentTypes(target) - f.deleteCalcChain(sheet.SheetID, "") - delete(f.sheetMap, sheet.Name) + f.deleteCalcChain(f.getSheetID(sheet), "") + delete(f.sheetMap, v.Name) f.Pkg.Delete(sheetXML) f.Pkg.Delete(rels) f.Relationships.Delete(rels) @@ -613,7 +630,7 @@ func (f *File) copySheet(from, to int) error { if rels, ok := f.Pkg.Load(fromRels); ok && rels != nil { f.Pkg.Store(toRels, rels.([]byte)) } - fromSheetXMLPath := f.sheetMap[trimSheetName(fromSheet)] + fromSheetXMLPath, _ := f.getSheetXMLPath(fromSheet) fromSheetAttr := f.xmlAttr[fromSheetXMLPath] f.xmlAttr[sheetXMLPath] = fromSheetAttr return err @@ -632,12 +649,12 @@ func (f *File) copySheet(from, to int) error { // // err := f.SetSheetVisible("Sheet1", false) // -func (f *File) SetSheetVisible(name string, visible bool) error { - name = trimSheetName(name) +func (f *File) SetSheetVisible(sheet string, visible bool) error { + sheet = trimSheetName(sheet) content := f.workbookReader() if visible { for k, v := range content.Sheets.Sheet { - if v.Name == name { + if strings.EqualFold(v.Name, sheet) { content.Sheets.Sheet[k].State = "" } } @@ -658,7 +675,7 @@ func (f *File) SetSheetVisible(name string, visible bool) error { if len(ws.SheetViews.SheetView) > 0 { tabSelected = ws.SheetViews.SheetView[0].TabSelected } - if v.Name == name && count > 1 && !tabSelected { + if strings.EqualFold(v.Name, sheet) && count > 1 && !tabSelected { content.Sheets.Sheet[k].State = "hidden" } } @@ -798,11 +815,10 @@ func (f *File) SetPanes(sheet, panes string) error { // // f.GetSheetVisible("Sheet1") // -func (f *File) GetSheetVisible(name string) bool { - content := f.workbookReader() - visible := false +func (f *File) GetSheetVisible(sheet string) bool { + content, name, visible := f.workbookReader(), trimSheetName(sheet), false for k, v := range content.Sheets.Sheet { - if v.Name == trimSheetName(name) { + if strings.EqualFold(v.Name, name) { if content.Sheets.Sheet[k].State == "" || content.Sheets.Sheet[k].State == "visible" { visible = true } @@ -834,7 +850,7 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { for _, r := range reg { regSearch = r } - name, ok := f.sheetMap[trimSheetName(sheet)] + name, ok := f.getSheetXMLPath(sheet) if !ok { return result, ErrSheetNotExist{sheet} } @@ -1609,7 +1625,7 @@ func (f *File) GroupSheets(sheets []string) error { sheetMap := f.GetSheetList() for idx, sheetName := range sheetMap { for _, s := range sheets { - if s == sheetName && idx == activeSheet { + if strings.EqualFold(s, sheetName) && idx == activeSheet { inActiveSheet = true } } diff --git a/sheetpr.go b/sheetpr.go index e8e6e9d431..73fb5b02c3 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -181,8 +181,8 @@ func (o *AutoPageBreaks) getSheetPrOption(pr *xlsxSheetPr) { // FitToPage(bool) // AutoPageBreaks(bool) // OutlineSummaryBelow(bool) -func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error { - ws, err := f.workSheetReader(name) +func (f *File) SetSheetPrOptions(sheet string, opts ...SheetPrOption) error { + ws, err := f.workSheetReader(sheet) if err != nil { return err } @@ -207,8 +207,8 @@ func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error { // FitToPage(bool) // AutoPageBreaks(bool) // OutlineSummaryBelow(bool) -func (f *File) GetSheetPrOptions(name string, opts ...SheetPrOptionPtr) error { - ws, err := f.workSheetReader(name) +func (f *File) GetSheetPrOptions(sheet string, opts ...SheetPrOptionPtr) error { + ws, err := f.workSheetReader(sheet) if err != nil { return err } diff --git a/sheetview.go b/sheetview.go index 99f0634ff0..05312ca1ed 100644 --- a/sheetview.go +++ b/sheetview.go @@ -200,8 +200,8 @@ func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) // // err = f.SetSheetViewOptions("Sheet1", -1, ShowGridLines(false)) // -func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetViewOption) error { - view, err := f.getSheetView(name, viewIndex) +func (f *File) SetSheetViewOptions(sheet string, viewIndex int, opts ...SheetViewOption) error { + view, err := f.getSheetView(sheet, viewIndex) if err != nil { return err } @@ -233,8 +233,8 @@ func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetView // var showGridLines excelize.ShowGridLines // err = f.GetSheetViewOptions("Sheet1", -1, &showGridLines) // -func (f *File) GetSheetViewOptions(name string, viewIndex int, opts ...SheetViewOptionPtr) error { - view, err := f.getSheetView(name, viewIndex) +func (f *File) GetSheetViewOptions(sheet string, viewIndex int, opts ...SheetViewOptionPtr) error { + view, err := f.getSheetView(sheet, viewIndex) if err != nil { return err } diff --git a/stream.go b/stream.go index 0db2438888..1a1af24412 100644 --- a/stream.go +++ b/stream.go @@ -106,11 +106,11 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { return nil, err } - sheetPath := f.sheetMap[trimSheetName(sheet)] + sheetXMLPath, _ := f.getSheetXMLPath(sheet) if f.streams == nil { f.streams = make(map[string]*StreamWriter) } - f.streams[sheetPath] = sw + f.streams[sheetXMLPath] = sw _, _ = sw.rawData.WriteString(xml.Header + ` Date: Tue, 26 Jul 2022 12:36:21 +0800 Subject: [PATCH 080/213] This closes #1283, support set and get color index, theme and tint for sheet tab This commit renames `TabColor` into `TabColorRGB` (but keeps an alias for backwards compatibility), as well as adds support for more tab color options (Theme, Indexed and Tint). Signed-off-by: Thomas Charbonnel --- sheetpr.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++--- sheetpr_test.go | 42 ++++++++++++++++++----- 2 files changed, 118 insertions(+), 14 deletions(-) diff --git a/sheetpr.go b/sheetpr.go index 73fb5b02c3..cc4e4a99a3 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -33,14 +33,37 @@ type ( Published bool // FitToPage is a SheetPrOption FitToPage bool - // TabColor is a SheetPrOption - TabColor string + // TabColorIndexed is a TabColor option, within SheetPrOption + TabColorIndexed int + // TabColorRGB is a TabColor option, within SheetPrOption + TabColorRGB string + // TabColorTheme is a TabColor option, within SheetPrOption + TabColorTheme int + // TabColorTint is a TabColor option, within SheetPrOption + TabColorTint float64 // AutoPageBreaks is a SheetPrOption AutoPageBreaks bool // OutlineSummaryBelow is an outlinePr, within SheetPr option OutlineSummaryBelow bool ) +const ( + TabColorThemeLight1 int = iota // Inverted compared to the spec because that's how Excel maps them + TabColorThemeDark1 + TabColorThemeLight2 + TabColorThemeDark2 + TabColorThemeAccent1 + TabColorThemeAccent2 + TabColorThemeAccent3 + TabColorThemeAccent4 + TabColorThemeAccent5 + TabColorThemeAccent6 + TabColorThemeHyperlink + TabColorThemeFollowedHyperlink + + TabColorUnsetValue int = -1 +) + // setSheetPrOption implements the SheetPrOption interface. func (o OutlineSummaryBelow) setSheetPrOption(pr *xlsxSheetPr) { if pr.OutlinePr == nil { @@ -129,9 +152,28 @@ func (o *FitToPage) getSheetPrOption(pr *xlsxSheetPr) { *o = FitToPage(pr.PageSetUpPr.FitToPage) } +// setSheetPrOption implements the SheetPrOption interface and sets the +// TabColor Indexed. +func (o TabColorIndexed) setSheetPrOption(pr *xlsxSheetPr) { + if pr.TabColor == nil { + pr.TabColor = new(xlsxTabColor) + } + pr.TabColor.Indexed = int(o) +} + +// getSheetPrOption implements the SheetPrOptionPtr interface and gets the +// TabColor Indexed. Defaults to -1 if no indexed has been set. +func (o *TabColorIndexed) getSheetPrOption(pr *xlsxSheetPr) { + if pr == nil || pr.TabColor == nil { + *o = TabColorIndexed(TabColorUnsetValue) + return + } + *o = TabColorIndexed(pr.TabColor.Indexed) +} + // setSheetPrOption implements the SheetPrOption interface and specifies a // stable name of the sheet. -func (o TabColor) setSheetPrOption(pr *xlsxSheetPr) { +func (o TabColorRGB) setSheetPrOption(pr *xlsxSheetPr) { if pr.TabColor == nil { if string(o) == "" { return @@ -143,12 +185,50 @@ func (o TabColor) setSheetPrOption(pr *xlsxSheetPr) { // getSheetPrOption implements the SheetPrOptionPtr interface and get the // stable name of the sheet. -func (o *TabColor) getSheetPrOption(pr *xlsxSheetPr) { +func (o *TabColorRGB) getSheetPrOption(pr *xlsxSheetPr) { if pr == nil || pr.TabColor == nil { *o = "" return } - *o = TabColor(strings.TrimPrefix(pr.TabColor.RGB, "FF")) + *o = TabColorRGB(strings.TrimPrefix(pr.TabColor.RGB, "FF")) +} + +// setSheetPrOption implements the SheetPrOption interface and sets the +// TabColor Theme. Warning: it does not create a clrScheme! +func (o TabColorTheme) setSheetPrOption(pr *xlsxSheetPr) { + if pr.TabColor == nil { + pr.TabColor = new(xlsxTabColor) + } + pr.TabColor.Theme = int(o) +} + +// getSheetPrOption implements the SheetPrOptionPtr interface and gets the +// TabColor Theme. Defaults to -1 if no theme has been set. +func (o *TabColorTheme) getSheetPrOption(pr *xlsxSheetPr) { + if pr == nil || pr.TabColor == nil { + *o = TabColorTheme(TabColorUnsetValue) + return + } + *o = TabColorTheme(pr.TabColor.Theme) +} + +// setSheetPrOption implements the SheetPrOption interface and sets the +// TabColor Tint. +func (o TabColorTint) setSheetPrOption(pr *xlsxSheetPr) { + if pr.TabColor == nil { + pr.TabColor = new(xlsxTabColor) + } + pr.TabColor.Tint = float64(o) +} + +// getSheetPrOption implements the SheetPrOptionPtr interface and gets the +// TabColor Tint. Defaults to 0.0 if no tint has been set. +func (o *TabColorTint) getSheetPrOption(pr *xlsxSheetPr) { + if pr == nil || pr.TabColor == nil { + *o = 0.0 + return + } + *o = TabColorTint(pr.TabColor.Tint) } // setSheetPrOption implements the SheetPrOption interface. diff --git a/sheetpr_test.go b/sheetpr_test.go index 91685d8837..000b33a529 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -13,7 +13,10 @@ var _ = []SheetPrOption{ EnableFormatConditionsCalculation(false), Published(false), FitToPage(true), - TabColor("#FFFF00"), + TabColorIndexed(42), + TabColorRGB("#FFFF00"), + TabColorTheme(TabColorThemeLight2), + TabColorTint(0.5), AutoPageBreaks(true), OutlineSummaryBelow(true), } @@ -23,7 +26,10 @@ var _ = []SheetPrOptionPtr{ (*EnableFormatConditionsCalculation)(nil), (*Published)(nil), (*FitToPage)(nil), - (*TabColor)(nil), + (*TabColorIndexed)(nil), + (*TabColorRGB)(nil), + (*TabColorTheme)(nil), + (*TabColorTint)(nil), (*AutoPageBreaks)(nil), (*OutlineSummaryBelow)(nil), } @@ -37,7 +43,10 @@ func ExampleFile_SetSheetPrOptions() { EnableFormatConditionsCalculation(false), Published(false), FitToPage(true), - TabColor("#FFFF00"), + TabColorIndexed(42), + TabColorRGB("#FFFF00"), + TabColorTheme(TabColorThemeLight2), + TabColorTint(0.5), AutoPageBreaks(true), OutlineSummaryBelow(false), ); err != nil { @@ -55,7 +64,10 @@ func ExampleFile_GetSheetPrOptions() { enableFormatConditionsCalculation EnableFormatConditionsCalculation published Published fitToPage FitToPage - tabColor TabColor + tabColorIndexed TabColorIndexed + tabColorRGB TabColorRGB + tabColorTheme TabColorTheme + tabColorTint TabColorTint autoPageBreaks AutoPageBreaks outlineSummaryBelow OutlineSummaryBelow ) @@ -65,7 +77,10 @@ func ExampleFile_GetSheetPrOptions() { &enableFormatConditionsCalculation, &published, &fitToPage, - &tabColor, + &tabColorIndexed, + &tabColorRGB, + &tabColorTheme, + &tabColorTint, &autoPageBreaks, &outlineSummaryBelow, ); err != nil { @@ -76,7 +91,10 @@ func ExampleFile_GetSheetPrOptions() { fmt.Println("- enableFormatConditionsCalculation:", enableFormatConditionsCalculation) fmt.Println("- published:", published) fmt.Println("- fitToPage:", fitToPage) - fmt.Printf("- tabColor: %q\n", tabColor) + fmt.Printf("- tabColorIndexed: %d\n", tabColorIndexed) + fmt.Printf("- tabColorRGB: %q\n", tabColorRGB) + fmt.Printf("- tabColorTheme: %d\n", tabColorTheme) + fmt.Printf("- tabColorTint: %f\n", tabColorTint) fmt.Println("- autoPageBreaks:", autoPageBreaks) fmt.Println("- outlineSummaryBelow:", outlineSummaryBelow) // Output: @@ -85,7 +103,10 @@ func ExampleFile_GetSheetPrOptions() { // - enableFormatConditionsCalculation: true // - published: true // - fitToPage: false - // - tabColor: "" + // - tabColorIndexed: -1 + // - tabColorRGB: "" + // - tabColorTheme: -1 + // - tabColorTint: 0.000000 // - autoPageBreaks: false // - outlineSummaryBelow: true } @@ -101,7 +122,10 @@ func TestSheetPrOptions(t *testing.T) { {new(EnableFormatConditionsCalculation), EnableFormatConditionsCalculation(false)}, {new(Published), Published(false)}, {new(FitToPage), FitToPage(true)}, - {new(TabColor), TabColor("FFFF00")}, + {new(TabColorIndexed), TabColorIndexed(42)}, + {new(TabColorRGB), TabColorRGB("FFFF00")}, + {new(TabColorTheme), TabColorTheme(TabColorThemeLight2)}, + {new(TabColorTint), TabColorTint(0.5)}, {new(AutoPageBreaks), AutoPageBreaks(true)}, {new(OutlineSummaryBelow), OutlineSummaryBelow(false)}, } @@ -154,7 +178,7 @@ func TestSheetPrOptions(t *testing.T) { func TestSetSheetPrOptions(t *testing.T) { f := NewFile() - assert.NoError(t, f.SetSheetPrOptions("Sheet1", TabColor(""))) + assert.NoError(t, f.SetSheetPrOptions("Sheet1", TabColorRGB(""))) // Test SetSheetPrOptions on not exists worksheet. assert.EqualError(t, f.SetSheetPrOptions("SheetN"), "sheet SheetN is not exist") } From 504d469d3da34602a9a88bd76669ce44fdbc67cf Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 3 Aug 2022 00:42:16 +0800 Subject: [PATCH 081/213] This closes #1298, fix doc properties missing after creating new worksheet --- sheet.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/sheet.go b/sheet.go index 51410b043f..01dd1672b9 100644 --- a/sheet.go +++ b/sheet.go @@ -53,8 +53,6 @@ func (f *File) NewSheet(sheet string) int { } } sheetID++ - // Update docProps/app.xml - f.setAppXML() // Update [Content_Types].xml f.setContentTypes("/xl/worksheets/sheet"+strconv.Itoa(sheetID)+".xml", ContentTypeSpreadSheetMLWorksheet) // Create new sheet /xl/worksheets/sheet%d.xml @@ -239,11 +237,6 @@ func (f *File) relsWriter() { }) } -// setAppXML update docProps/app.xml file of XML. -func (f *File) setAppXML() { - f.saveFileList(defaultXMLPathDocPropsApp, []byte(templateDocpropsApp)) -} - // replaceRelationshipsBytes; Some tools that read spreadsheet files have very // strict requirements about the structure of the input XML. This function is // a horrible hack to fix that after the XML marshalling is completed. From 4a029f7e3602ac48b6fbf410b86adac2af64983a Mon Sep 17 00:00:00 2001 From: Thomas Charbonnel Date: Thu, 4 Aug 2022 16:50:33 +0800 Subject: [PATCH 082/213] This closes #1299 skip write nil values in SetRow (#1301) Co-authored-by: Thomas Charbonnel --- stream.go | 3 +++ stream_test.go | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/stream.go b/stream.go index 1a1af24412..52e65a46c8 100644 --- a/stream.go +++ b/stream.go @@ -327,6 +327,9 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}, opts ...RowOpt } fmt.Fprintf(&sw.rawData, ``, row, attrs) for i, val := range values { + if val == nil { + continue + } axis, err := CoordinatesToCellName(col+i, row) if err != nil { return err diff --git a/stream_test.go b/stream_test.go index 6843e2064f..8f6a5b4cf5 100644 --- a/stream_test.go +++ b/stream_test.go @@ -209,6 +209,17 @@ func TestSetRow(t *testing.T) { assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } +func TestSetRowNilValues(t *testing.T) { + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + streamWriter.SetRow("A1", []interface{}{nil, nil, Cell{Value: "foo"}}) + streamWriter.Flush() + ws, err := file.workSheetReader("Sheet1") + assert.NoError(t, err) + assert.NotEqual(t, ws.SheetData.Row[0].C[0].XMLName.Local, "c") +} + func TestSetCellValFunc(t *testing.T) { f := NewFile() sw, err := f.NewStreamWriter("Sheet1") From e07dac5c2e308c952c8fdec5e6ad7089ff432ccf Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 6 Aug 2022 15:23:03 +0800 Subject: [PATCH 083/213] Add new exported `ColorMappingType` used for color transformation Using `ColorMappingType` color transformation types enumeration for tab color index, ref #1285 --- sheetpr.go | 21 ++------------------- sheetpr_test.go | 6 +++--- xmlDrawing.go | 20 ++++++++++++++++++++ 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/sheetpr.go b/sheetpr.go index cc4e4a99a3..0e3cb9b7c8 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -47,23 +47,6 @@ type ( OutlineSummaryBelow bool ) -const ( - TabColorThemeLight1 int = iota // Inverted compared to the spec because that's how Excel maps them - TabColorThemeDark1 - TabColorThemeLight2 - TabColorThemeDark2 - TabColorThemeAccent1 - TabColorThemeAccent2 - TabColorThemeAccent3 - TabColorThemeAccent4 - TabColorThemeAccent5 - TabColorThemeAccent6 - TabColorThemeHyperlink - TabColorThemeFollowedHyperlink - - TabColorUnsetValue int = -1 -) - // setSheetPrOption implements the SheetPrOption interface. func (o OutlineSummaryBelow) setSheetPrOption(pr *xlsxSheetPr) { if pr.OutlinePr == nil { @@ -165,7 +148,7 @@ func (o TabColorIndexed) setSheetPrOption(pr *xlsxSheetPr) { // TabColor Indexed. Defaults to -1 if no indexed has been set. func (o *TabColorIndexed) getSheetPrOption(pr *xlsxSheetPr) { if pr == nil || pr.TabColor == nil { - *o = TabColorIndexed(TabColorUnsetValue) + *o = TabColorIndexed(ColorMappingTypeUnset) return } *o = TabColorIndexed(pr.TabColor.Indexed) @@ -206,7 +189,7 @@ func (o TabColorTheme) setSheetPrOption(pr *xlsxSheetPr) { // TabColor Theme. Defaults to -1 if no theme has been set. func (o *TabColorTheme) getSheetPrOption(pr *xlsxSheetPr) { if pr == nil || pr.TabColor == nil { - *o = TabColorTheme(TabColorUnsetValue) + *o = TabColorTheme(ColorMappingTypeUnset) return } *o = TabColorTheme(pr.TabColor.Theme) diff --git a/sheetpr_test.go b/sheetpr_test.go index 000b33a529..65ab196f38 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -15,7 +15,7 @@ var _ = []SheetPrOption{ FitToPage(true), TabColorIndexed(42), TabColorRGB("#FFFF00"), - TabColorTheme(TabColorThemeLight2), + TabColorTheme(ColorMappingTypeLight2), TabColorTint(0.5), AutoPageBreaks(true), OutlineSummaryBelow(true), @@ -45,7 +45,7 @@ func ExampleFile_SetSheetPrOptions() { FitToPage(true), TabColorIndexed(42), TabColorRGB("#FFFF00"), - TabColorTheme(TabColorThemeLight2), + TabColorTheme(ColorMappingTypeLight2), TabColorTint(0.5), AutoPageBreaks(true), OutlineSummaryBelow(false), @@ -124,7 +124,7 @@ func TestSheetPrOptions(t *testing.T) { {new(FitToPage), FitToPage(true)}, {new(TabColorIndexed), TabColorIndexed(42)}, {new(TabColorRGB), TabColorRGB("FFFF00")}, - {new(TabColorTheme), TabColorTheme(TabColorThemeLight2)}, + {new(TabColorTheme), TabColorTheme(ColorMappingTypeLight2)}, {new(TabColorTint), TabColorTint(0.5)}, {new(AutoPageBreaks), AutoPageBreaks(true)}, {new(OutlineSummaryBelow), OutlineSummaryBelow(false)}, diff --git a/xmlDrawing.go b/xmlDrawing.go index 3e54b7207f..8c3d73442f 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -120,6 +120,26 @@ const ( pivotTableVersion = 3 ) +// ColorMappingType is the type of color transformation. +type ColorMappingType byte + +// Color transformation types enumeration. +const ( + ColorMappingTypeLight1 ColorMappingType = iota + ColorMappingTypeDark1 + ColorMappingTypeLight2 + ColorMappingTypeDark2 + ColorMappingTypeAccent1 + ColorMappingTypeAccent2 + ColorMappingTypeAccent3 + ColorMappingTypeAccent4 + ColorMappingTypeAccent5 + ColorMappingTypeAccent6 + ColorMappingTypeHyperlink + ColorMappingTypeFollowedHyperlink + ColorMappingTypeUnset int = -1 +) + // supportedImageTypes defined supported image types. var supportedImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff", ".emf": ".emf", ".wmf": ".wmf"} From b8ceaf7bf61daecad8717abec90a8e0badb64806 Mon Sep 17 00:00:00 2001 From: EE Date: Wed, 10 Aug 2022 10:35:33 +0800 Subject: [PATCH 084/213] This reduces memory usage and speedup the `AddComment` function (#1311) By load only once for existing comment shapes, improving performance for adding comments in the worksheet --- comment.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/comment.go b/comment.go index 23f1079ad0..03b12155ec 100644 --- a/comment.go +++ b/comment.go @@ -178,6 +178,21 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, }, }, } + // load exist comment shapes from xl/drawings/vmlDrawing%d.vml (only once) + d := f.decodeVMLDrawingReader(drawingVML) + if d != nil { + for _, v := range d.Shape { + s := xlsxShape{ + ID: "_x0000_s1025", + Type: "#_x0000_t202", + Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden", + Fillcolor: "#fbf6d6", + Strokecolor: "#edeaa1", + Val: v.Val, + } + vml.Shape = append(vml.Shape, s) + } + } } sp := encodeShape{ Fill: &vFill{ @@ -222,20 +237,6 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, Strokecolor: "#edeaa1", Val: string(s[13 : len(s)-14]), } - d := f.decodeVMLDrawingReader(drawingVML) - if d != nil { - for _, v := range d.Shape { - s := xlsxShape{ - ID: "_x0000_s1025", - Type: "#_x0000_t202", - Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden", - Fillcolor: "#fbf6d6", - Strokecolor: "#edeaa1", - Val: v.Val, - } - vml.Shape = append(vml.Shape, s) - } - } vml.Shape = append(vml.Shape, shape) f.VMLDrawing[drawingVML] = vml return err From ed91cddea59ce15da87ab744ac20b465a36ed5ef Mon Sep 17 00:00:00 2001 From: Thomas Charbonnel Date: Thu, 11 Aug 2022 00:20:48 +0800 Subject: [PATCH 085/213] This closes #1296, add new function `GetRowOpts` for stream reader (#1297) - Support get rows properties by `GetRowOpts` function - New exported constant `MaxCellStyles` --- rows.go | 24 ++++++++++++++++++++++++ rows_test.go | 24 ++++++++++++++++++++++++ sheet.go | 28 ++++++++++++++++++++++++++++ sheet_test.go | 26 ++++++++++++++++++++++++++ test/Book1.xlsx | Bin 20738 -> 20451 bytes xmlDrawing.go | 1 + 6 files changed, 103 insertions(+) diff --git a/rows.go b/rows.go index 853c8f7df3..457f59b729 100644 --- a/rows.go +++ b/rows.go @@ -80,12 +80,14 @@ type Rows struct { sst *xlsxSST decoder *xml.Decoder token xml.Token + curRowOpts, seekRowOpts RowOpts } // Next will return true if find the next row element. func (rows *Rows) Next() bool { rows.seekRow++ if rows.curRow >= rows.seekRow { + rows.curRowOpts = rows.seekRowOpts return true } for { @@ -101,6 +103,7 @@ func (rows *Rows) Next() bool { rows.curRow = rowNum } rows.token = token + rows.curRowOpts = extractRowOpts(xmlElement.Attr) return true } case xml.EndElement: @@ -111,6 +114,11 @@ func (rows *Rows) Next() bool { } } +// GetRowOpts will return the RowOpts of the current row. +func (rows *Rows) GetRowOpts() RowOpts { + return rows.curRowOpts +} + // Error will return the error when the error occurs. func (rows *Rows) Error() error { return rows.err @@ -151,6 +159,8 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) { } else if rows.token == nil { rows.curRow++ } + rows.token = token + rows.seekRowOpts = extractRowOpts(xmlElement.Attr) if rows.curRow > rows.seekRow { rows.token = nil return rowIterator.columns, rowIterator.err @@ -170,6 +180,20 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) { return rowIterator.columns, rowIterator.err } +func extractRowOpts(attrs []xml.Attr) RowOpts { + rowOpts := RowOpts{Height: defaultRowHeight} + if styleID, err := attrValToInt("s", attrs); err == nil && styleID > 0 && styleID < MaxCellStyles { + rowOpts.StyleID = styleID + } + if hidden, err := attrValToBool("hidden", attrs); err == nil { + rowOpts.Hidden = hidden + } + if height, err := attrValToFloat("ht", attrs); err == nil { + rowOpts.Height = height + } + return rowOpts +} + // appendSpace append blank characters to slice by given length and source slice. func appendSpace(l int, s []string) []string { for i := 1; i < l; i++ { diff --git a/rows_test.go b/rows_test.go index 4fe28517cd..4b57c34109 100644 --- a/rows_test.go +++ b/rows_test.go @@ -96,6 +96,30 @@ func TestRowsIterator(t *testing.T) { assert.Equal(t, expectedNumRow, rowCount) } +func TestRowsGetRowOpts(t *testing.T) { + sheetName := "Sheet2" + expectedRowStyleID1 := RowOpts{Height: 17.0, Hidden: false, StyleID: 1} + expectedRowStyleID2 := RowOpts{Height: 17.0, Hidden: false, StyleID: 0} + expectedRowStyleID3 := RowOpts{Height: 17.0, Hidden: false, StyleID: 2} + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + require.NoError(t, err) + + rows, err := f.Rows(sheetName) + require.NoError(t, err) + + rows.Next() + rows.Columns() // Columns() may change the XML iterator, so better check with and without calling it + got := rows.GetRowOpts() + assert.Equal(t, expectedRowStyleID1, got) + rows.Next() + got = rows.GetRowOpts() + assert.Equal(t, expectedRowStyleID2, got) + rows.Next() + rows.Columns() + got = rows.GetRowOpts() + assert.Equal(t, expectedRowStyleID3, got) +} + func TestRowsError(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { diff --git a/sheet.go b/sheet.go index 01dd1672b9..1f2dceaaaa 100644 --- a/sheet.go +++ b/sheet.go @@ -928,6 +928,34 @@ func attrValToInt(name string, attrs []xml.Attr) (val int, err error) { return } +// attrValToFloat provides a function to convert the local names to a float64 +// by given XML attributes and specified names. +func attrValToFloat(name string, attrs []xml.Attr) (val float64, err error) { + for _, attr := range attrs { + if attr.Name.Local == name { + val, err = strconv.ParseFloat(attr.Value, 64) + if err != nil { + return + } + } + } + return +} + +// attrValToBool provides a function to convert the local names to a boolean +// by given XML attributes and specified names. +func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { + for _, attr := range attrs { + if attr.Name.Local == name { + val, err = strconv.ParseBool(attr.Value) + if err != nil { + return + } + } + } + return +} + // SetHeaderFooter provides a function to set headers and footers by given // worksheet name and the control characters. // diff --git a/sheet_test.go b/sheet_test.go index c68ad3129f..9b0caf4894 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -505,3 +505,29 @@ func newSheetWithSave() { } _ = file.Save() } + +func TestAttrValToBool(t *testing.T) { + _, err := attrValToBool("hidden", []xml.Attr{ + {Name: xml.Name{Local: "hidden"}}, + }) + assert.EqualError(t, err, `strconv.ParseBool: parsing "": invalid syntax`) + + got, err := attrValToBool("hidden", []xml.Attr{ + {Name: xml.Name{Local: "hidden"}, Value: "1"}, + }) + assert.NoError(t, err) + assert.Equal(t, true, got) +} + +func TestAttrValToFloat(t *testing.T) { + _, err := attrValToFloat("ht", []xml.Attr{ + {Name: xml.Name{Local: "ht"}}, + }) + assert.EqualError(t, err, `strconv.ParseFloat: parsing "": invalid syntax`) + + got, err := attrValToFloat("ht", []xml.Attr{ + {Name: xml.Name{Local: "ht"}, Value: "42.1"}, + }) + assert.NoError(t, err) + assert.Equal(t, 42.1, got) +} diff --git a/test/Book1.xlsx b/test/Book1.xlsx index 6a497e33afb45a5764a323326bd1f2f57480184d..ed3e29295461c903ad4511cc7d434b18297b293f 100644 GIT binary patch delta 2144 zcmaJ>YgAKL7QQzm*1jaWA^0;6~1r(4ef`~#3R#4RNl2@yPMhBFhP-aQinl(S}S?Al|x4*sjJ!|ch zbEqaCrHlOy@MZ`?A|Yr*JwweXf>e^r=^E5xJ|+H2R6l|shyVwexc7uv3Z(OC z7~ddEUzx-!+wKib2-oML2-bVwK$k!R?1Uj*pPMG0eAfawB?_LBEJ2z-1xyg{ z&Q&V#r-4Dz*c=*%$W&m4nNnU6=0a=%4O?R}W;h2E1T>ILHvwU^S05u-G99O1Qyy$v z%ON00!740oaWFw|;=OMNV-Y-poMT}$>m_(S?nmmaAL($$-%xDJvNigiXBvCzYv3Fu zC@8fZ(o1V9xr5=7VXEcd;`F2&rO#hhkPw7N#`f6XBQfT`#lz`@ z4ghWbV}KxL2>2kfz+}#u|FyFUpkZs*Z8p%eQ$?q;Va+#)P zj&@}8uUS)lM8rct+3VMGytipWK9$ThX;lX7K}D%XEk))&R2nXbwd&$8k%XD|hqOl) z9?hucC)C?lq_q3oQjT(Ve7Sd;+n>rSFtIOBQJ?m53~JjBk! z<#{t%Bn;Z(k~bdq4C$-_V+tbfp zzIBfr2;`1Ma|bZ=Sb@OehIf<86B=JjrNnpav}{aaM>k~c$jV(&eM!7c*6)+?s)BT(&4l3 z%zJGD+Qtq`gMBN6Geu*9#n-RnK2(GK9BRp&mvJZSpRtJsUfjo?Yz;w<6)SyO$LjI7}E+|HFn zcuUA+BDLHdi<1~}H*b7iR%+{`;a~D*mF6W{DYTR3`@RTh>U38r8v-YOb?9}@>8ZLB zbvUNK&987sC2qI;xh{6<(;TgO=)sDl{e&dh$bp&sLnqX1mT&3l-Q$@q41vuVW!D9T zyj$Do6H(FAINUHI99ClIa~=lf*M9KG?NZ#yjlY3kJN$K$sbYe-n^D%GO~GB`Jds+4 zDiYR{Zek6->8_h14;Kcuo{&?$+RBm-NI6f~#CP@Yt!mQTqBONr=IWOCzU?+MFL&*( zs(Dz|JZ#rL71)2Nr8V{OBI^B5|Cx2Z^QQE4s`cF_!DQOtBhD``&IVsz*r0st`Qi4o z=Sq}vD(k^jX?A!1(J%ioCVBERSrrv@*tR5d>D8;tpFIlt#^+yWVvS{vBl~_I<@BLq ze-vj1ml6WZ_?{>NLitSkJGdhv68FT)*sFEtM@c%Li7o(Td`l?23-9${g1-dbjr^H7 zXG5SgTtfaR4u-)v6v@F z5HM}BEfC~jEPIzEGI76*y-OM^O_Vw6me9wlP=>3ff$mu&Ed*V3zz{BluC78R>H)S1 zbrs}7CN9ASZuF@9VVln2`7K?09J0KIWpdbqh+&%*}BAsUs z1WYHm&~?BnfQcJ)2A%;l2*Mx;;{bVpj>(1qxPTc~^LeH{Fqq>GJ_Ia=3c8qyyX*-z zigi6iLqPQcrQVEHy1|wjz~}j&YuFXW5d^*#a^a}T2PT-?&Fy5upF8>g?PD+`{)ECH KA_&IQx%>ecMfd^$ delta 2774 zcma);dpwl+9>?c~$2AG#HuNyq#+~D~MH$6h7!zCA?6%`t%ot`Un=);YZBKG}Qqc;r z#D+3XvQbEzERF5;3cDhbOD-u%DRO=@TP^3j%sJ0L&+YyF{662$?|J=x&rm-4$OW`6 zla9ryqaX-Exv+EnP|J>WNIymMicK5ZL=FnN-#glhMxkotQK;1_khPdafHDsCsPKDvC0W1{nkJ2&%b-yG+N;Y7%h$|^ z+48D_+AdiXN|mrWi*({OkycHy4L$YJixa>`w3G&gop?wb#{b1W)AEx6Y@#2r!4X(4 zFkjt<&|hB<2iWj8BWx57g}PuYg*|R)E%xxsy2f9$K)Tb?5*8Va0`%;y-llrcT7?u; z*iICQWp9^XRHy>n5=d^fi4$HiIf()61q*aV6!>;t0n4khJ2u>d0K1X<_(e5@LQ$lA z?m}nVDzS&2h1mND@bFgJRJ^wf!U9(RnFY=ha8`;^Jj6H}i*;*R_KEU9w@+FQKReBc zr|l*4^SW*@ZOJ-PbcL>SVqLc{EGr%XO>Jp8+}yHL%zD%oTrvZ!qBPcwEXjASQ{6Ti ztDBuB6vNikHwHMV6^`gKq2DhBohg;6RL%cgAluC|Exkl0=T1JE zImSBmw4glhkm%fX4MTft$ZVgYYzIH{H8heSP?C$c-Bojm5tq$(j0r-@27q@p3{(vER)YEuEOz$Upi0kc-8Van5H?axy#(Z#f?@zilwfOTSyD%Gobf5We<8ha)-BFuV>zw z4vcS{Ae30|YPqJImNxF7ZDHevx$9hnPkqYlUmw43kc2yV`dFJInYg}MJZkk`u>6VwWDj{&wxn$hcj7BvzrIa^? z#>C4h<`!@hlcMFVDF28^tVszt{;at$qjkK;6e9PO$ zxl9Y(m&23`g;@~8EGIp?K0)~E;Y<9r3cKv~)Q9L3CUtm0)!^4X56^FF(DHQ`*iah| zD!;1UMrgZQb$K`=QQOD&jw7?y@z9dU#Cj*2TNm%~ zMrDGXup_Wht-$~FpDAPEglGLz3kE)q@zY0NTE6JZTY7IgM!l6NNL!eAojE)=-U;9G zhadSE)&_npr*}ga<%#AP^bP73HBS~BgdE&cdP-}sf%t{ zD9M>unhomo5Is@eUrbIS=Iv-5H3@IaI zS&`5fbrkp+K&>IYEQKi)zDvQQI`Sko0SBs{W-E+#%c2+%?V zk_Y*87(NE};{>)<$Q~9ij&H?}`<5MMv(nxmpqBLii?Urw8F|43H>1BOE{78q9moF< z9hMi2IYUH}y)1|y|FD*CSy~^%m7I%>kxrrxGVLYqq`(A^tnpSvnRJ_L4G5pZpaaWHI2a zXPqQ(<5Dqm~|_B4>==MP8!SR@hAhfX%f&Fa~i)tjId%pBniv=JJnoWmcXZpg=>sl^`8HN(zHu|5v1mUHu!X C@kS&7 diff --git a/xmlDrawing.go b/xmlDrawing.go index 8c3d73442f..b4fdccc88e 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -107,6 +107,7 @@ const ( MaxFieldLength = 255 MaxColumnWidth = 255 MaxRowHeight = 409 + MaxCellStyles = 64000 MinFontSize = 1 TotalRows = 1048576 MinColumns = 1 From 8152bbb2cec76f074dc18c43f3c66bf8abdf9de0 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 12 Aug 2022 00:32:51 +0800 Subject: [PATCH 086/213] This closes #1312, #1313, fix number format issue - Add supported options in the docs of the functions `SetSheetPrOptions` and `GetSheetPrOptions` - Add go1.19 unit test settings, and made the test case compatible with go1.19 - Update dependencies module --- .github/workflows/go.yml | 2 +- go.mod | 4 ++-- go.sum | 10 +++++----- lib_test.go | 4 ++++ numfmt.go | 4 ++-- rows_test.go | 24 +++++++++++++----------- sheetpr.go | 8 ++++++++ 7 files changed, 35 insertions(+), 21 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a81d4044da..4026b719ba 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,7 +5,7 @@ jobs: test: strategy: matrix: - go-version: [1.15.x, 1.16.x, 1.17.x, 1.18.x] + go-version: [1.15.x, 1.16.x, 1.17.x, 1.18.x, 1.19.x] os: [ubuntu-latest, macos-latest, windows-latest] targetplatform: [x86, x64] diff --git a/go.mod b/go.mod index 4d628fcf0f..0fda81006d 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,9 @@ require ( github.com/stretchr/testify v1.7.1 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-20220622213112-05595931fe9d + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 - golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e + golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index 3ffe339485..a79ea1f2e4 100644 --- a/go.sum +++ b/go.sum @@ -17,17 +17,17 @@ github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj0 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-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/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-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced h1:3dYNDff0VT5xj+mbj2XucFst9WKk6PdGOrb9n+SbIvw= +golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/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= diff --git a/lib_test.go b/lib_test.go index 64acb8ae61..5fa644eaf0 100644 --- a/lib_test.go +++ b/lib_test.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "runtime" "strconv" "strings" "sync" @@ -340,6 +341,9 @@ func TestReadBytes(t *testing.T) { } func TestUnzipToTemp(t *testing.T) { + if strings.HasPrefix(runtime.Version(), "go1.19") { + t.Skip() + } os.Setenv("TMPDIR", "test") defer os.Unsetenv("TMPDIR") assert.NoError(t, os.Chmod(os.TempDir(), 0o444)) diff --git a/numfmt.go b/numfmt.go index 6b4fc65399..56f354f1f9 100644 --- a/numfmt.go +++ b/numfmt.go @@ -337,7 +337,7 @@ func (nf *numberFormat) positiveHandler() (result string) { nf.result += token.TValue continue } - if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == "0" { + if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) { if isNum, precision := isNumeric(nf.value); isNum { if nf.number < 1 { nf.result += "0" @@ -899,7 +899,7 @@ func (nf *numberFormat) negativeHandler() (result string) { nf.result += token.TValue continue } - if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == "0" { + if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) { if isNum, precision := isNumeric(nf.value); isNum { if math.Abs(nf.number) < 1 { nf.result += "0" diff --git a/rows_test.go b/rows_test.go index 4b57c34109..585401b52f 100644 --- a/rows_test.go +++ b/rows_test.go @@ -107,17 +107,19 @@ func TestRowsGetRowOpts(t *testing.T) { rows, err := f.Rows(sheetName) require.NoError(t, err) - rows.Next() - rows.Columns() // Columns() may change the XML iterator, so better check with and without calling it - got := rows.GetRowOpts() - assert.Equal(t, expectedRowStyleID1, got) - rows.Next() - got = rows.GetRowOpts() - assert.Equal(t, expectedRowStyleID2, got) - rows.Next() - rows.Columns() - got = rows.GetRowOpts() - assert.Equal(t, expectedRowStyleID3, got) + assert.Equal(t, true, rows.Next()) + _, err = rows.Columns() + require.NoError(t, err) + rowOpts := rows.GetRowOpts() + assert.Equal(t, expectedRowStyleID1, rowOpts) + assert.Equal(t, true, rows.Next()) + rowOpts = rows.GetRowOpts() + assert.Equal(t, expectedRowStyleID2, rowOpts) + assert.Equal(t, true, rows.Next()) + _, err = rows.Columns() + require.NoError(t, err) + rowOpts = rows.GetRowOpts() + assert.Equal(t, expectedRowStyleID3, rowOpts) } func TestRowsError(t *testing.T) { diff --git a/sheetpr.go b/sheetpr.go index 0e3cb9b7c8..65939c16d0 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -242,6 +242,10 @@ func (o *AutoPageBreaks) getSheetPrOption(pr *xlsxSheetPr) { // EnableFormatConditionsCalculation(bool) // Published(bool) // FitToPage(bool) +// TabColorIndexed(int) +// TabColorRGB(string) +// TabColorTheme(int) +// TabColorTint(float64) // AutoPageBreaks(bool) // OutlineSummaryBelow(bool) func (f *File) SetSheetPrOptions(sheet string, opts ...SheetPrOption) error { @@ -268,6 +272,10 @@ func (f *File) SetSheetPrOptions(sheet string, opts ...SheetPrOption) error { // EnableFormatConditionsCalculation(bool) // Published(bool) // FitToPage(bool) +// TabColorIndexed(int) +// TabColorRGB(string) +// TabColorTheme(int) +// TabColorTint(float64) // AutoPageBreaks(bool) // OutlineSummaryBelow(bool) func (f *File) GetSheetPrOptions(sheet string, opts ...SheetPrOptionPtr) error { From 551fb8a9e4b03fe718a339e75aeacc8b5581378a Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 13 Aug 2022 11:21:59 +0800 Subject: [PATCH 087/213] This closes #1244 and closes #1314, improving the compatibility with Google Sheet - Format code with `gofmt` --- adjust.go | 1 - calc.go | 2184 ++++++++++++++++++--------------------------- cell.go | 386 ++++---- chart.go | 569 ++++++------ col.go | 132 ++- comment.go | 3 +- crypt.go | 7 +- datavalidation.go | 38 +- docProps.go | 170 ++-- excelize.go | 50 +- file.go | 3 +- lib.go | 21 +- merge.go | 27 +- picture.go | 129 ++- pivotTable.go | 112 ++- rows.go | 121 ++- shape.go | 465 +++++----- sheet.go | 600 ++++++------- sheetpr.go | 98 +- sheetview.go | 48 +- sparkline.go | 39 +- stream.go | 94 +- styles.go | 1913 ++++++++++++++++++++------------------- table.go | 83 +- workbook.go | 14 +- xmlCalcChain.go | 107 ++- xmlWorksheet.go | 35 +- 27 files changed, 3474 insertions(+), 3975 deletions(-) diff --git a/adjust.go b/adjust.go index d766b3ebf8..99d2850913 100644 --- a/adjust.go +++ b/adjust.go @@ -35,7 +35,6 @@ const ( // offset: Number of rows/column to insert/delete negative values indicate deletion // // TODO: adjustPageBreaks, adjustComments, adjustDataValidations, adjustProtectedCells -// func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error { ws, err := f.workSheetReader(sheet) if err != nil { diff --git a/calc.go b/calc.go index d8161cef01..0cdb91ed6d 100644 --- a/calc.go +++ b/calc.go @@ -331,441 +331,440 @@ type formulaFuncs struct { // // Supported formula functions: // -// ABS -// ACCRINT -// ACCRINTM -// ACOS -// ACOSH -// ACOT -// ACOTH -// ADDRESS -// AMORDEGRC -// AMORLINC -// AND -// ARABIC -// ASIN -// ASINH -// ATAN -// ATAN2 -// ATANH -// AVEDEV -// AVERAGE -// AVERAGEA -// AVERAGEIF -// AVERAGEIFS -// BASE -// BESSELI -// BESSELJ -// BESSELK -// BESSELY -// BETADIST -// BETA.DIST -// BETAINV -// BETA.INV -// BIN2DEC -// BIN2HEX -// BIN2OCT -// BINOMDIST -// BINOM.DIST -// BINOM.DIST.RANGE -// BINOM.INV -// BITAND -// BITLSHIFT -// BITOR -// BITRSHIFT -// BITXOR -// CEILING -// CEILING.MATH -// CEILING.PRECISE -// CHAR -// CHIDIST -// CHIINV -// CHITEST -// CHISQ.DIST -// CHISQ.DIST.RT -// CHISQ.INV -// CHISQ.INV.RT -// CHISQ.TEST -// CHOOSE -// CLEAN -// CODE -// COLUMN -// COLUMNS -// COMBIN -// COMBINA -// COMPLEX -// CONCAT -// CONCATENATE -// CONFIDENCE -// CONFIDENCE.NORM -// CONFIDENCE.T -// CONVERT -// CORREL -// COS -// COSH -// COT -// COTH -// COUNT -// COUNTA -// COUNTBLANK -// COUNTIF -// COUNTIFS -// COUPDAYBS -// COUPDAYS -// COUPDAYSNC -// COUPNCD -// COUPNUM -// COUPPCD -// COVAR -// COVARIANCE.P -// COVARIANCE.S -// CRITBINOM -// CSC -// CSCH -// CUMIPMT -// CUMPRINC -// DATE -// DATEDIF -// DATEVALUE -// DAVERAGE -// DAY -// DAYS -// DAYS360 -// DB -// DCOUNT -// DCOUNTA -// DDB -// DEC2BIN -// DEC2HEX -// DEC2OCT -// DECIMAL -// DEGREES -// DELTA -// DEVSQ -// DGET -// DISC -// DMAX -// DMIN -// DOLLARDE -// DOLLARFR -// DPRODUCT -// DSTDEV -// DSTDEVP -// DSUM -// DURATION -// DVAR -// DVARP -// EFFECT -// EDATE -// ENCODEURL -// EOMONTH -// ERF -// ERF.PRECISE -// ERFC -// ERFC.PRECISE -// ERROR.TYPE -// EUROCONVERT -// EVEN -// EXACT -// EXP -// EXPON.DIST -// EXPONDIST -// FACT -// FACTDOUBLE -// FALSE -// F.DIST -// F.DIST.RT -// FDIST -// FIND -// FINDB -// F.INV -// F.INV.RT -// FINV -// FISHER -// FISHERINV -// FIXED -// FLOOR -// FLOOR.MATH -// FLOOR.PRECISE -// FORMULATEXT -// F.TEST -// FTEST -// FV -// FVSCHEDULE -// GAMMA -// GAMMA.DIST -// GAMMADIST -// GAMMA.INV -// GAMMAINV -// GAMMALN -// GAMMALN.PRECISE -// GAUSS -// GCD -// GEOMEAN -// GESTEP -// GROWTH -// HARMEAN -// HEX2BIN -// HEX2DEC -// HEX2OCT -// HLOOKUP -// HOUR -// HYPERLINK -// HYPGEOM.DIST -// HYPGEOMDIST -// IF -// IFERROR -// IFNA -// IFS -// IMABS -// IMAGINARY -// IMARGUMENT -// IMCONJUGATE -// IMCOS -// IMCOSH -// IMCOT -// IMCSC -// IMCSCH -// IMDIV -// IMEXP -// IMLN -// IMLOG10 -// IMLOG2 -// IMPOWER -// IMPRODUCT -// IMREAL -// IMSEC -// IMSECH -// IMSIN -// IMSINH -// IMSQRT -// IMSUB -// IMSUM -// IMTAN -// INDEX -// INDIRECT -// INT -// INTRATE -// IPMT -// IRR -// ISBLANK -// ISERR -// ISERROR -// ISEVEN -// ISFORMULA -// ISLOGICAL -// ISNA -// ISNONTEXT -// ISNUMBER -// ISODD -// ISREF -// ISTEXT -// ISO.CEILING -// ISOWEEKNUM -// ISPMT -// KURT -// LARGE -// LCM -// LEFT -// LEFTB -// LEN -// LENB -// LN -// LOG -// LOG10 -// LOGINV -// LOGNORM.DIST -// LOGNORMDIST -// LOGNORM.INV -// LOOKUP -// LOWER -// MATCH -// MAX -// MAXA -// MAXIFS -// MDETERM -// MDURATION -// MEDIAN -// MID -// MIDB -// MIN -// MINA -// MINIFS -// MINUTE -// MINVERSE -// MIRR -// MMULT -// MOD -// MODE -// MODE.MULT -// MODE.SNGL -// MONTH -// MROUND -// MULTINOMIAL -// MUNIT -// N -// NA -// NEGBINOM.DIST -// NEGBINOMDIST -// NETWORKDAYS -// NETWORKDAYS.INTL -// NOMINAL -// NORM.DIST -// NORMDIST -// NORM.INV -// NORMINV -// NORM.S.DIST -// NORMSDIST -// NORM.S.INV -// NORMSINV -// NOT -// NOW -// NPER -// NPV -// OCT2BIN -// OCT2DEC -// OCT2HEX -// ODD -// ODDFPRICE -// OR -// PDURATION -// PEARSON -// PERCENTILE.EXC -// PERCENTILE.INC -// PERCENTILE -// PERCENTRANK.EXC -// PERCENTRANK.INC -// PERCENTRANK -// PERMUT -// PERMUTATIONA -// PHI -// PI -// PMT -// POISSON.DIST -// POISSON -// POWER -// PPMT -// PRICE -// PRICEDISC -// PRICEMAT -// PRODUCT -// PROPER -// PV -// QUARTILE -// QUARTILE.EXC -// QUARTILE.INC -// QUOTIENT -// RADIANS -// RAND -// RANDBETWEEN -// RANK -// RANK.EQ -// RATE -// RECEIVED -// REPLACE -// REPLACEB -// REPT -// RIGHT -// RIGHTB -// ROMAN -// ROUND -// ROUNDDOWN -// ROUNDUP -// ROW -// ROWS -// RRI -// RSQ -// SEC -// SECH -// SECOND -// SERIESSUM -// SHEET -// SHEETS -// SIGN -// SIN -// SINH -// SKEW -// SKEW.P -// SLN -// SLOPE -// SMALL -// SQRT -// SQRTPI -// STANDARDIZE -// STDEV -// STDEV.P -// STDEV.S -// STDEVA -// STDEVP -// STDEVPA -// STEYX -// SUBSTITUTE -// SUM -// SUMIF -// SUMIFS -// SUMPRODUCT -// SUMSQ -// SUMX2MY2 -// SUMX2PY2 -// SUMXMY2 -// SWITCH -// SYD -// T -// TAN -// TANH -// TBILLEQ -// TBILLPRICE -// TBILLYIELD -// T.DIST -// T.DIST.2T -// T.DIST.RT -// TDIST -// TEXTJOIN -// TIME -// TIMEVALUE -// T.INV -// T.INV.2T -// TINV -// TODAY -// TRANSPOSE -// TREND -// TRIM -// TRIMMEAN -// TRUE -// TRUNC -// T.TEST -// TTEST -// TYPE -// UNICHAR -// UNICODE -// UPPER -// VALUE -// VAR -// VAR.P -// VAR.S -// VARA -// VARP -// VARPA -// VDB -// VLOOKUP -// WEEKDAY -// WEEKNUM -// WEIBULL -// WEIBULL.DIST -// WORKDAY -// WORKDAY.INTL -// XIRR -// XLOOKUP -// XNPV -// XOR -// YEAR -// YEARFRAC -// YIELD -// YIELDDISC -// YIELDMAT -// Z.TEST -// ZTEST -// +// ABS +// ACCRINT +// ACCRINTM +// ACOS +// ACOSH +// ACOT +// ACOTH +// ADDRESS +// AMORDEGRC +// AMORLINC +// AND +// ARABIC +// ASIN +// ASINH +// ATAN +// ATAN2 +// ATANH +// AVEDEV +// AVERAGE +// AVERAGEA +// AVERAGEIF +// AVERAGEIFS +// BASE +// BESSELI +// BESSELJ +// BESSELK +// BESSELY +// BETADIST +// BETA.DIST +// BETAINV +// BETA.INV +// BIN2DEC +// BIN2HEX +// BIN2OCT +// BINOMDIST +// BINOM.DIST +// BINOM.DIST.RANGE +// BINOM.INV +// BITAND +// BITLSHIFT +// BITOR +// BITRSHIFT +// BITXOR +// CEILING +// CEILING.MATH +// CEILING.PRECISE +// CHAR +// CHIDIST +// CHIINV +// CHITEST +// CHISQ.DIST +// CHISQ.DIST.RT +// CHISQ.INV +// CHISQ.INV.RT +// CHISQ.TEST +// CHOOSE +// CLEAN +// CODE +// COLUMN +// COLUMNS +// COMBIN +// COMBINA +// COMPLEX +// CONCAT +// CONCATENATE +// CONFIDENCE +// CONFIDENCE.NORM +// CONFIDENCE.T +// CONVERT +// CORREL +// COS +// COSH +// COT +// COTH +// COUNT +// COUNTA +// COUNTBLANK +// COUNTIF +// COUNTIFS +// COUPDAYBS +// COUPDAYS +// COUPDAYSNC +// COUPNCD +// COUPNUM +// COUPPCD +// COVAR +// COVARIANCE.P +// COVARIANCE.S +// CRITBINOM +// CSC +// CSCH +// CUMIPMT +// CUMPRINC +// DATE +// DATEDIF +// DATEVALUE +// DAVERAGE +// DAY +// DAYS +// DAYS360 +// DB +// DCOUNT +// DCOUNTA +// DDB +// DEC2BIN +// DEC2HEX +// DEC2OCT +// DECIMAL +// DEGREES +// DELTA +// DEVSQ +// DGET +// DISC +// DMAX +// DMIN +// DOLLARDE +// DOLLARFR +// DPRODUCT +// DSTDEV +// DSTDEVP +// DSUM +// DURATION +// DVAR +// DVARP +// EFFECT +// EDATE +// ENCODEURL +// EOMONTH +// ERF +// ERF.PRECISE +// ERFC +// ERFC.PRECISE +// ERROR.TYPE +// EUROCONVERT +// EVEN +// EXACT +// EXP +// EXPON.DIST +// EXPONDIST +// FACT +// FACTDOUBLE +// FALSE +// F.DIST +// F.DIST.RT +// FDIST +// FIND +// FINDB +// F.INV +// F.INV.RT +// FINV +// FISHER +// FISHERINV +// FIXED +// FLOOR +// FLOOR.MATH +// FLOOR.PRECISE +// FORMULATEXT +// F.TEST +// FTEST +// FV +// FVSCHEDULE +// GAMMA +// GAMMA.DIST +// GAMMADIST +// GAMMA.INV +// GAMMAINV +// GAMMALN +// GAMMALN.PRECISE +// GAUSS +// GCD +// GEOMEAN +// GESTEP +// GROWTH +// HARMEAN +// HEX2BIN +// HEX2DEC +// HEX2OCT +// HLOOKUP +// HOUR +// HYPERLINK +// HYPGEOM.DIST +// HYPGEOMDIST +// IF +// IFERROR +// IFNA +// IFS +// IMABS +// IMAGINARY +// IMARGUMENT +// IMCONJUGATE +// IMCOS +// IMCOSH +// IMCOT +// IMCSC +// IMCSCH +// IMDIV +// IMEXP +// IMLN +// IMLOG10 +// IMLOG2 +// IMPOWER +// IMPRODUCT +// IMREAL +// IMSEC +// IMSECH +// IMSIN +// IMSINH +// IMSQRT +// IMSUB +// IMSUM +// IMTAN +// INDEX +// INDIRECT +// INT +// INTRATE +// IPMT +// IRR +// ISBLANK +// ISERR +// ISERROR +// ISEVEN +// ISFORMULA +// ISLOGICAL +// ISNA +// ISNONTEXT +// ISNUMBER +// ISODD +// ISREF +// ISTEXT +// ISO.CEILING +// ISOWEEKNUM +// ISPMT +// KURT +// LARGE +// LCM +// LEFT +// LEFTB +// LEN +// LENB +// LN +// LOG +// LOG10 +// LOGINV +// LOGNORM.DIST +// LOGNORMDIST +// LOGNORM.INV +// LOOKUP +// LOWER +// MATCH +// MAX +// MAXA +// MAXIFS +// MDETERM +// MDURATION +// MEDIAN +// MID +// MIDB +// MIN +// MINA +// MINIFS +// MINUTE +// MINVERSE +// MIRR +// MMULT +// MOD +// MODE +// MODE.MULT +// MODE.SNGL +// MONTH +// MROUND +// MULTINOMIAL +// MUNIT +// N +// NA +// NEGBINOM.DIST +// NEGBINOMDIST +// NETWORKDAYS +// NETWORKDAYS.INTL +// NOMINAL +// NORM.DIST +// NORMDIST +// NORM.INV +// NORMINV +// NORM.S.DIST +// NORMSDIST +// NORM.S.INV +// NORMSINV +// NOT +// NOW +// NPER +// NPV +// OCT2BIN +// OCT2DEC +// OCT2HEX +// ODD +// ODDFPRICE +// OR +// PDURATION +// PEARSON +// PERCENTILE.EXC +// PERCENTILE.INC +// PERCENTILE +// PERCENTRANK.EXC +// PERCENTRANK.INC +// PERCENTRANK +// PERMUT +// PERMUTATIONA +// PHI +// PI +// PMT +// POISSON.DIST +// POISSON +// POWER +// PPMT +// PRICE +// PRICEDISC +// PRICEMAT +// PRODUCT +// PROPER +// PV +// QUARTILE +// QUARTILE.EXC +// QUARTILE.INC +// QUOTIENT +// RADIANS +// RAND +// RANDBETWEEN +// RANK +// RANK.EQ +// RATE +// RECEIVED +// REPLACE +// REPLACEB +// REPT +// RIGHT +// RIGHTB +// ROMAN +// ROUND +// ROUNDDOWN +// ROUNDUP +// ROW +// ROWS +// RRI +// RSQ +// SEC +// SECH +// SECOND +// SERIESSUM +// SHEET +// SHEETS +// SIGN +// SIN +// SINH +// SKEW +// SKEW.P +// SLN +// SLOPE +// SMALL +// SQRT +// SQRTPI +// STANDARDIZE +// STDEV +// STDEV.P +// STDEV.S +// STDEVA +// STDEVP +// STDEVPA +// STEYX +// SUBSTITUTE +// SUM +// SUMIF +// SUMIFS +// SUMPRODUCT +// SUMSQ +// SUMX2MY2 +// SUMX2PY2 +// SUMXMY2 +// SWITCH +// SYD +// T +// TAN +// TANH +// TBILLEQ +// TBILLPRICE +// TBILLYIELD +// T.DIST +// T.DIST.2T +// T.DIST.RT +// TDIST +// TEXTJOIN +// TIME +// TIMEVALUE +// T.INV +// T.INV.2T +// TINV +// TODAY +// TRANSPOSE +// TREND +// TRIM +// TRIMMEAN +// TRUE +// TRUNC +// T.TEST +// TTEST +// TYPE +// UNICHAR +// UNICODE +// UPPER +// VALUE +// VAR +// VAR.P +// VAR.S +// VARA +// VARP +// VARPA +// VDB +// VLOOKUP +// WEEKDAY +// WEEKNUM +// WEIBULL +// WEIBULL.DIST +// WORKDAY +// WORKDAY.INTL +// XIRR +// XLOOKUP +// XNPV +// XOR +// YEAR +// YEARFRAC +// YIELD +// YIELDDISC +// YIELDMAT +// Z.TEST +// ZTEST func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { return f.calcCellValue(&calcContext{ entry: fmt.Sprintf("%s!%s", sheet, cell), @@ -857,15 +856,14 @@ func newEmptyFormulaArg() formulaArg { // lexical analysis. Evaluate an infix expression containing formulas by // stacks: // -// opd - Operand -// opt - Operator -// opf - Operation formula -// opfd - Operand of the operation formula -// opft - Operator of the operation formula -// args - Arguments list of the operation formula +// opd - Operand +// opt - Operator +// opf - Operation formula +// opfd - Operand of the operation formula +// opft - Operator of the operation formula +// args - Arguments list of the operation formula // // TODO: handle subtypes: Nothing, Text, Logical, Error, Concatenation, Intersection, Union -// func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.Token) (formulaArg, error) { var err error opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack() @@ -1692,8 +1690,7 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er // Bessel function evaluated for purely imaginary arguments. The syntax of // the Besseli function is: // -// BESSELI(x,n) -// +// BESSELI(x,n) func (fn *formulaFuncs) BESSELI(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "BESSELI requires 2 numeric arguments") @@ -1704,8 +1701,7 @@ func (fn *formulaFuncs) BESSELI(argsList *list.List) formulaArg { // BESSELJ function returns the Bessel function, Jn(x), for a specified order // and value of x. The syntax of the function is: // -// BESSELJ(x,n) -// +// BESSELJ(x,n) func (fn *formulaFuncs) BESSELJ(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "BESSELJ requires 2 numeric arguments") @@ -1752,8 +1748,7 @@ func (fn *formulaFuncs) bassel(argsList *list.List, modfied bool) formulaArg { // the Bessel functions, evaluated for purely imaginary arguments. The syntax // of the function is: // -// BESSELK(x,n) -// +// BESSELK(x,n) func (fn *formulaFuncs) BESSELK(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "BESSELK requires 2 numeric arguments") @@ -1832,8 +1827,7 @@ func (fn *formulaFuncs) besselK2(x, n formulaArg) float64 { // Weber function or the Neumann function), for a specified order and value // of x. The syntax of the function is: // -// BESSELY(x,n) -// +// BESSELY(x,n) func (fn *formulaFuncs) BESSELY(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "BESSELY requires 2 numeric arguments") @@ -1913,8 +1907,7 @@ func (fn *formulaFuncs) besselY2(x, n formulaArg) float64 { // BIN2DEC function converts a Binary (a base-2 number) into a decimal number. // The syntax of the function is: // -// BIN2DEC(number) -// +// BIN2DEC(number) func (fn *formulaFuncs) BIN2DEC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "BIN2DEC requires 1 numeric argument") @@ -1930,8 +1923,7 @@ func (fn *formulaFuncs) BIN2DEC(argsList *list.List) formulaArg { // BIN2HEX function converts a Binary (Base 2) number into a Hexadecimal // (Base 16) number. The syntax of the function is: // -// BIN2HEX(number,[places]) -// +// BIN2HEX(number,[places]) func (fn *formulaFuncs) BIN2HEX(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "BIN2HEX requires at least 1 argument") @@ -1958,8 +1950,7 @@ func (fn *formulaFuncs) BIN2HEX(argsList *list.List) formulaArg { // BIN2OCT function converts a Binary (Base 2) number into an Octal (Base 8) // number. The syntax of the function is: // -// BIN2OCT(number,[places]) -// +// BIN2OCT(number,[places]) func (fn *formulaFuncs) BIN2OCT(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "BIN2OCT requires at least 1 argument") @@ -2006,8 +1997,7 @@ func (fn *formulaFuncs) bin2dec(number string) formulaArg { // BITAND function returns the bitwise 'AND' for two supplied integers. The // syntax of the function is: // -// BITAND(number1,number2) -// +// BITAND(number1,number2) func (fn *formulaFuncs) BITAND(argsList *list.List) formulaArg { return fn.bitwise("BITAND", argsList) } @@ -2015,8 +2005,7 @@ func (fn *formulaFuncs) BITAND(argsList *list.List) formulaArg { // BITLSHIFT function returns a supplied integer, shifted left by a specified // number of bits. The syntax of the function is: // -// BITLSHIFT(number1,shift_amount) -// +// BITLSHIFT(number1,shift_amount) func (fn *formulaFuncs) BITLSHIFT(argsList *list.List) formulaArg { return fn.bitwise("BITLSHIFT", argsList) } @@ -2024,8 +2013,7 @@ func (fn *formulaFuncs) BITLSHIFT(argsList *list.List) formulaArg { // BITOR function returns the bitwise 'OR' for two supplied integers. The // syntax of the function is: // -// BITOR(number1,number2) -// +// BITOR(number1,number2) func (fn *formulaFuncs) BITOR(argsList *list.List) formulaArg { return fn.bitwise("BITOR", argsList) } @@ -2033,8 +2021,7 @@ func (fn *formulaFuncs) BITOR(argsList *list.List) formulaArg { // BITRSHIFT function returns a supplied integer, shifted right by a specified // number of bits. The syntax of the function is: // -// BITRSHIFT(number1,shift_amount) -// +// BITRSHIFT(number1,shift_amount) func (fn *formulaFuncs) BITRSHIFT(argsList *list.List) formulaArg { return fn.bitwise("BITRSHIFT", argsList) } @@ -2042,8 +2029,7 @@ func (fn *formulaFuncs) BITRSHIFT(argsList *list.List) formulaArg { // BITXOR function returns the bitwise 'XOR' (exclusive 'OR') for two supplied // integers. The syntax of the function is: // -// BITXOR(number1,number2) -// +// BITXOR(number1,number2) func (fn *formulaFuncs) BITXOR(argsList *list.List) formulaArg { return fn.bitwise("BITXOR", argsList) } @@ -2077,8 +2063,7 @@ func (fn *formulaFuncs) bitwise(name string, argsList *list.List) formulaArg { // imaginary coefficients of a complex number, and from these, creates a // complex number. The syntax of the function is: // -// COMPLEX(real_num,i_num,[suffix]) -// +// COMPLEX(real_num,i_num,[suffix]) func (fn *formulaFuncs) COMPLEX(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "COMPLEX requires at least 2 arguments") @@ -2631,8 +2616,7 @@ func convertTemperature(fromUOM, toUOM string, value float64) float64 { // CONVERT function converts a number from one unit type (e.g. Yards) to // another unit type (e.g. Meters). The syntax of the function is: // -// CONVERT(number,from_unit,to_unit) -// +// CONVERT(number,from_unit,to_unit) func (fn *formulaFuncs) CONVERT(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "CONVERT requires 3 arguments") @@ -2663,8 +2647,7 @@ func (fn *formulaFuncs) CONVERT(argsList *list.List) formulaArg { // DEC2BIN function converts a decimal number into a Binary (Base 2) number. // The syntax of the function is: // -// DEC2BIN(number,[places]) -// +// DEC2BIN(number,[places]) func (fn *formulaFuncs) DEC2BIN(argsList *list.List) formulaArg { return fn.dec2x("DEC2BIN", argsList) } @@ -2672,8 +2655,7 @@ func (fn *formulaFuncs) DEC2BIN(argsList *list.List) formulaArg { // DEC2HEX function converts a decimal number into a Hexadecimal (Base 16) // number. The syntax of the function is: // -// DEC2HEX(number,[places]) -// +// DEC2HEX(number,[places]) func (fn *formulaFuncs) DEC2HEX(argsList *list.List) formulaArg { return fn.dec2x("DEC2HEX", argsList) } @@ -2681,8 +2663,7 @@ func (fn *formulaFuncs) DEC2HEX(argsList *list.List) formulaArg { // DEC2OCT function converts a decimal number into an Octal (Base 8) number. // The syntax of the function is: // -// DEC2OCT(number,[places]) -// +// DEC2OCT(number,[places]) func (fn *formulaFuncs) DEC2OCT(argsList *list.List) formulaArg { return fn.dec2x("DEC2OCT", argsList) } @@ -2761,8 +2742,7 @@ func (fn *formulaFuncs) dec2x(name string, argsList *list.List) formulaArg { // Delta. i.e. the function returns 1 if the two supplied numbers are equal // and 0 otherwise. The syntax of the function is: // -// DELTA(number1,[number2]) -// +// DELTA(number1,[number2]) func (fn *formulaFuncs) DELTA(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "DELTA requires at least 1 argument") @@ -2786,8 +2766,7 @@ func (fn *formulaFuncs) DELTA(argsList *list.List) formulaArg { // ERF function calculates the Error Function, integrated between two supplied // limits. The syntax of the function is: // -// ERF(lower_limit,[upper_limit]) -// +// ERF(lower_limit,[upper_limit]) func (fn *formulaFuncs) ERF(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "ERF requires at least 1 argument") @@ -2812,8 +2791,7 @@ func (fn *formulaFuncs) ERF(argsList *list.List) formulaArg { // ERFdotPRECISE function calculates the Error Function, integrated between a // supplied lower or upper limit and 0. The syntax of the function is: // -// ERF.PRECISE(x) -// +// ERF.PRECISE(x) func (fn *formulaFuncs) ERFdotPRECISE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ERF.PRECISE requires 1 argument") @@ -2841,8 +2819,7 @@ func (fn *formulaFuncs) erfc(name string, argsList *list.List) formulaArg { // between a supplied lower limit and infinity. The syntax of the function // is: // -// ERFC(x) -// +// ERFC(x) func (fn *formulaFuncs) ERFC(argsList *list.List) formulaArg { return fn.erfc("ERFC", argsList) } @@ -2851,8 +2828,7 @@ func (fn *formulaFuncs) ERFC(argsList *list.List) formulaArg { // integrated between a supplied lower limit and infinity. The syntax of the // function is: // -// ERFC(x) -// +// ERFC(x) func (fn *formulaFuncs) ERFCdotPRECISE(argsList *list.List) formulaArg { return fn.erfc("ERFC.PRECISE", argsList) } @@ -2860,8 +2836,7 @@ func (fn *formulaFuncs) ERFCdotPRECISE(argsList *list.List) formulaArg { // GESTEP unction tests whether a supplied number is greater than a supplied // step size and returns. The syntax of the function is: // -// GESTEP(number,[step]) -// +// GESTEP(number,[step]) func (fn *formulaFuncs) GESTEP(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "GESTEP requires at least 1 argument") @@ -2885,8 +2860,7 @@ func (fn *formulaFuncs) GESTEP(argsList *list.List) formulaArg { // HEX2BIN function converts a Hexadecimal (Base 16) number into a Binary // (Base 2) number. The syntax of the function is: // -// HEX2BIN(number,[places]) -// +// HEX2BIN(number,[places]) func (fn *formulaFuncs) HEX2BIN(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "HEX2BIN requires at least 1 argument") @@ -2908,8 +2882,7 @@ func (fn *formulaFuncs) HEX2BIN(argsList *list.List) formulaArg { // HEX2DEC function converts a hexadecimal (a base-16 number) into a decimal // number. The syntax of the function is: // -// HEX2DEC(number) -// +// HEX2DEC(number) func (fn *formulaFuncs) HEX2DEC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "HEX2DEC requires 1 numeric argument") @@ -2920,8 +2893,7 @@ func (fn *formulaFuncs) HEX2DEC(argsList *list.List) formulaArg { // HEX2OCT function converts a Hexadecimal (Base 16) number into an Octal // (Base 8) number. The syntax of the function is: // -// HEX2OCT(number,[places]) -// +// HEX2OCT(number,[places]) func (fn *formulaFuncs) HEX2OCT(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "HEX2OCT requires at least 1 argument") @@ -2960,8 +2932,7 @@ func (fn *formulaFuncs) hex2dec(number string) formulaArg { // IMABS function returns the absolute value (the modulus) of a complex // number. The syntax of the function is: // -// IMABS(inumber) -// +// IMABS(inumber) func (fn *formulaFuncs) IMABS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMABS requires 1 argument") @@ -2977,8 +2948,7 @@ func (fn *formulaFuncs) IMABS(argsList *list.List) formulaArg { // IMAGINARY function returns the imaginary coefficient of a supplied complex // number. The syntax of the function is: // -// IMAGINARY(inumber) -// +// IMAGINARY(inumber) func (fn *formulaFuncs) IMAGINARY(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMAGINARY requires 1 argument") @@ -2994,8 +2964,7 @@ func (fn *formulaFuncs) IMAGINARY(argsList *list.List) formulaArg { // IMARGUMENT function returns the phase (also called the argument) of a // supplied complex number. The syntax of the function is: // -// IMARGUMENT(inumber) -// +// IMARGUMENT(inumber) func (fn *formulaFuncs) IMARGUMENT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMARGUMENT requires 1 argument") @@ -3011,8 +2980,7 @@ func (fn *formulaFuncs) IMARGUMENT(argsList *list.List) formulaArg { // IMCONJUGATE function returns the complex conjugate of a supplied complex // number. The syntax of the function is: // -// IMCONJUGATE(inumber) -// +// IMCONJUGATE(inumber) func (fn *formulaFuncs) IMCONJUGATE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCONJUGATE requires 1 argument") @@ -3028,8 +2996,7 @@ func (fn *formulaFuncs) IMCONJUGATE(argsList *list.List) formulaArg { // IMCOS function returns the cosine of a supplied complex number. The syntax // of the function is: // -// IMCOS(inumber) -// +// IMCOS(inumber) func (fn *formulaFuncs) IMCOS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCOS requires 1 argument") @@ -3045,8 +3012,7 @@ func (fn *formulaFuncs) IMCOS(argsList *list.List) formulaArg { // IMCOSH function returns the hyperbolic cosine of a supplied complex number. The syntax // of the function is: // -// IMCOSH(inumber) -// +// IMCOSH(inumber) func (fn *formulaFuncs) IMCOSH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCOSH requires 1 argument") @@ -3062,8 +3028,7 @@ func (fn *formulaFuncs) IMCOSH(argsList *list.List) formulaArg { // IMCOT function returns the cotangent of a supplied complex number. The syntax // of the function is: // -// IMCOT(inumber) -// +// IMCOT(inumber) func (fn *formulaFuncs) IMCOT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCOT requires 1 argument") @@ -3079,8 +3044,7 @@ func (fn *formulaFuncs) IMCOT(argsList *list.List) formulaArg { // IMCSC function returns the cosecant of a supplied complex number. The syntax // of the function is: // -// IMCSC(inumber) -// +// IMCSC(inumber) func (fn *formulaFuncs) IMCSC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCSC requires 1 argument") @@ -3100,8 +3064,7 @@ func (fn *formulaFuncs) IMCSC(argsList *list.List) formulaArg { // IMCSCH function returns the hyperbolic cosecant of a supplied complex // number. The syntax of the function is: // -// IMCSCH(inumber) -// +// IMCSCH(inumber) func (fn *formulaFuncs) IMCSCH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCSCH requires 1 argument") @@ -3121,8 +3084,7 @@ func (fn *formulaFuncs) IMCSCH(argsList *list.List) formulaArg { // IMDIV function calculates the quotient of two complex numbers (i.e. divides // one complex number by another). The syntax of the function is: // -// IMDIV(inumber1,inumber2) -// +// IMDIV(inumber1,inumber2) func (fn *formulaFuncs) IMDIV(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "IMDIV requires 2 arguments") @@ -3146,8 +3108,7 @@ func (fn *formulaFuncs) IMDIV(argsList *list.List) formulaArg { // IMEXP function returns the exponential of a supplied complex number. The // syntax of the function is: // -// IMEXP(inumber) -// +// IMEXP(inumber) func (fn *formulaFuncs) IMEXP(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMEXP requires 1 argument") @@ -3163,8 +3124,7 @@ func (fn *formulaFuncs) IMEXP(argsList *list.List) formulaArg { // IMLN function returns the natural logarithm of a supplied complex number. // The syntax of the function is: // -// IMLN(inumber) -// +// IMLN(inumber) func (fn *formulaFuncs) IMLN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMLN requires 1 argument") @@ -3184,8 +3144,7 @@ func (fn *formulaFuncs) IMLN(argsList *list.List) formulaArg { // IMLOG10 function returns the common (base 10) logarithm of a supplied // complex number. The syntax of the function is: // -// IMLOG10(inumber) -// +// IMLOG10(inumber) func (fn *formulaFuncs) IMLOG10(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMLOG10 requires 1 argument") @@ -3205,8 +3164,7 @@ func (fn *formulaFuncs) IMLOG10(argsList *list.List) formulaArg { // IMLOG2 function calculates the base 2 logarithm of a supplied complex // number. The syntax of the function is: // -// IMLOG2(inumber) -// +// IMLOG2(inumber) func (fn *formulaFuncs) IMLOG2(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMLOG2 requires 1 argument") @@ -3226,8 +3184,7 @@ func (fn *formulaFuncs) IMLOG2(argsList *list.List) formulaArg { // IMPOWER function returns a supplied complex number, raised to a given // power. The syntax of the function is: // -// IMPOWER(inumber,number) -// +// IMPOWER(inumber,number) func (fn *formulaFuncs) IMPOWER(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "IMPOWER requires 2 arguments") @@ -3254,8 +3211,7 @@ func (fn *formulaFuncs) IMPOWER(argsList *list.List) formulaArg { // IMPRODUCT function calculates the product of two or more complex numbers. // The syntax of the function is: // -// IMPRODUCT(number1,[number2],...) -// +// IMPRODUCT(number1,[number2],...) func (fn *formulaFuncs) IMPRODUCT(argsList *list.List) formulaArg { product := complex128(1) for arg := argsList.Front(); arg != nil; arg = arg.Next() { @@ -3293,8 +3249,7 @@ func (fn *formulaFuncs) IMPRODUCT(argsList *list.List) formulaArg { // IMREAL function returns the real coefficient of a supplied complex number. // The syntax of the function is: // -// IMREAL(inumber) -// +// IMREAL(inumber) func (fn *formulaFuncs) IMREAL(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMREAL requires 1 argument") @@ -3310,8 +3265,7 @@ func (fn *formulaFuncs) IMREAL(argsList *list.List) formulaArg { // IMSEC function returns the secant of a supplied complex number. The syntax // of the function is: // -// IMSEC(inumber) -// +// IMSEC(inumber) func (fn *formulaFuncs) IMSEC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMSEC requires 1 argument") @@ -3327,8 +3281,7 @@ func (fn *formulaFuncs) IMSEC(argsList *list.List) formulaArg { // IMSECH function returns the hyperbolic secant of a supplied complex number. // The syntax of the function is: // -// IMSECH(inumber) -// +// IMSECH(inumber) func (fn *formulaFuncs) IMSECH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMSECH requires 1 argument") @@ -3344,8 +3297,7 @@ func (fn *formulaFuncs) IMSECH(argsList *list.List) formulaArg { // IMSIN function returns the Sine of a supplied complex number. The syntax of // the function is: // -// IMSIN(inumber) -// +// IMSIN(inumber) func (fn *formulaFuncs) IMSIN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMSIN requires 1 argument") @@ -3361,8 +3313,7 @@ func (fn *formulaFuncs) IMSIN(argsList *list.List) formulaArg { // IMSINH function returns the hyperbolic sine of a supplied complex number. // The syntax of the function is: // -// IMSINH(inumber) -// +// IMSINH(inumber) func (fn *formulaFuncs) IMSINH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMSINH requires 1 argument") @@ -3378,8 +3329,7 @@ func (fn *formulaFuncs) IMSINH(argsList *list.List) formulaArg { // IMSQRT function returns the square root of a supplied complex number. The // syntax of the function is: // -// IMSQRT(inumber) -// +// IMSQRT(inumber) func (fn *formulaFuncs) IMSQRT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMSQRT requires 1 argument") @@ -3396,8 +3346,7 @@ func (fn *formulaFuncs) IMSQRT(argsList *list.List) formulaArg { // (i.e. subtracts one complex number from another). The syntax of the // function is: // -// IMSUB(inumber1,inumber2) -// +// IMSUB(inumber1,inumber2) func (fn *formulaFuncs) IMSUB(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "IMSUB requires 2 arguments") @@ -3416,8 +3365,7 @@ func (fn *formulaFuncs) IMSUB(argsList *list.List) formulaArg { // IMSUM function calculates the sum of two or more complex numbers. The // syntax of the function is: // -// IMSUM(inumber1,inumber2,...) -// +// IMSUM(inumber1,inumber2,...) func (fn *formulaFuncs) IMSUM(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMSUM requires at least 1 argument") @@ -3437,8 +3385,7 @@ func (fn *formulaFuncs) IMSUM(argsList *list.List) formulaArg { // IMTAN function returns the tangent of a supplied complex number. The syntax // of the function is: // -// IMTAN(inumber) -// +// IMTAN(inumber) func (fn *formulaFuncs) IMTAN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMTAN requires 1 argument") @@ -3454,8 +3401,7 @@ func (fn *formulaFuncs) IMTAN(argsList *list.List) formulaArg { // OCT2BIN function converts an Octal (Base 8) number into a Binary (Base 2) // number. The syntax of the function is: // -// OCT2BIN(number,[places]) -// +// OCT2BIN(number,[places]) func (fn *formulaFuncs) OCT2BIN(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "OCT2BIN requires at least 1 argument") @@ -3479,8 +3425,7 @@ func (fn *formulaFuncs) OCT2BIN(argsList *list.List) formulaArg { // OCT2DEC function converts an Octal (a base-8 number) into a decimal number. // The syntax of the function is: // -// OCT2DEC(number) -// +// OCT2DEC(number) func (fn *formulaFuncs) OCT2DEC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "OCT2DEC requires 1 numeric argument") @@ -3496,8 +3441,7 @@ func (fn *formulaFuncs) OCT2DEC(argsList *list.List) formulaArg { // OCT2HEX function converts an Octal (Base 8) number into a Hexadecimal // (Base 16) number. The syntax of the function is: // -// OCT2HEX(number,[places]) -// +// OCT2HEX(number,[places]) func (fn *formulaFuncs) OCT2HEX(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "OCT2HEX requires at least 1 argument") @@ -3537,8 +3481,7 @@ func (fn *formulaFuncs) oct2dec(number string) formulaArg { // ABS function returns the absolute value of any supplied number. The syntax // of the function is: // -// ABS(number) -// +// ABS(number) func (fn *formulaFuncs) ABS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ABS requires 1 numeric argument") @@ -3554,8 +3497,7 @@ func (fn *formulaFuncs) ABS(argsList *list.List) formulaArg { // number, and returns an angle, in radians, between 0 and π. The syntax of // the function is: // -// ACOS(number) -// +// ACOS(number) func (fn *formulaFuncs) ACOS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ACOS requires 1 numeric argument") @@ -3570,8 +3512,7 @@ func (fn *formulaFuncs) ACOS(argsList *list.List) formulaArg { // ACOSH function calculates the inverse hyperbolic cosine of a supplied number. // of the function is: // -// ACOSH(number) -// +// ACOSH(number) func (fn *formulaFuncs) ACOSH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ACOSH requires 1 numeric argument") @@ -3587,8 +3528,7 @@ func (fn *formulaFuncs) ACOSH(argsList *list.List) formulaArg { // given number, and returns an angle, in radians, between 0 and π. The syntax // of the function is: // -// ACOT(number) -// +// ACOT(number) func (fn *formulaFuncs) ACOT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ACOT requires 1 numeric argument") @@ -3603,8 +3543,7 @@ func (fn *formulaFuncs) ACOT(argsList *list.List) formulaArg { // ACOTH function calculates the hyperbolic arccotangent (coth) of a supplied // value. The syntax of the function is: // -// ACOTH(number) -// +// ACOTH(number) func (fn *formulaFuncs) ACOTH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ACOTH requires 1 numeric argument") @@ -3619,8 +3558,7 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) formulaArg { // ARABIC function converts a Roman numeral into an Arabic numeral. The syntax // of the function is: // -// ARABIC(text) -// +// ARABIC(text) func (fn *formulaFuncs) ARABIC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ARABIC requires 1 numeric argument") @@ -3673,8 +3611,7 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) formulaArg { // number, and returns an angle, in radians, between -π/2 and π/2. The syntax // of the function is: // -// ASIN(number) -// +// ASIN(number) func (fn *formulaFuncs) ASIN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ASIN requires 1 numeric argument") @@ -3689,8 +3626,7 @@ func (fn *formulaFuncs) ASIN(argsList *list.List) formulaArg { // ASINH function calculates the inverse hyperbolic sine of a supplied number. // The syntax of the function is: // -// ASINH(number) -// +// ASINH(number) func (fn *formulaFuncs) ASINH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ASINH requires 1 numeric argument") @@ -3706,8 +3642,7 @@ func (fn *formulaFuncs) ASINH(argsList *list.List) formulaArg { // given number, and returns an angle, in radians, between -π/2 and +π/2. The // syntax of the function is: // -// ATAN(number) -// +// ATAN(number) func (fn *formulaFuncs) ATAN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ATAN requires 1 numeric argument") @@ -3722,8 +3657,7 @@ func (fn *formulaFuncs) ATAN(argsList *list.List) formulaArg { // ATANH function calculates the inverse hyperbolic tangent of a supplied // number. The syntax of the function is: // -// ATANH(number) -// +// ATANH(number) func (fn *formulaFuncs) ATANH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ATANH requires 1 numeric argument") @@ -3739,8 +3673,7 @@ func (fn *formulaFuncs) ATANH(argsList *list.List) formulaArg { // given set of x and y coordinates, and returns an angle, in radians, between // -π/2 and +π/2. The syntax of the function is: // -// ATAN2(x_num,y_num) -// +// ATAN2(x_num,y_num) func (fn *formulaFuncs) ATAN2(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "ATAN2 requires 2 numeric arguments") @@ -3759,8 +3692,7 @@ func (fn *formulaFuncs) ATAN2(argsList *list.List) formulaArg { // BASE function converts a number into a supplied base (radix), and returns a // text representation of the calculated value. The syntax of the function is: // -// BASE(number,radix,[min_length]) -// +// BASE(number,radix,[min_length]) func (fn *formulaFuncs) BASE(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "BASE requires at least 2 arguments") @@ -3796,8 +3728,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) formulaArg { // CEILING function rounds a supplied number away from zero, to the nearest // multiple of a given number. The syntax of the function is: // -// CEILING(number,significance) -// +// CEILING(number,significance) func (fn *formulaFuncs) CEILING(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "CEILING requires at least 1 argument") @@ -3837,8 +3768,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) formulaArg { // CEILINGdotMATH function rounds a supplied number up to a supplied multiple // of significance. The syntax of the function is: // -// CEILING.MATH(number,[significance],[mode]) -// +// CEILING.MATH(number,[significance],[mode]) func (fn *formulaFuncs) CEILINGdotMATH(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "CEILING.MATH requires at least 1 argument") @@ -3887,8 +3817,7 @@ func (fn *formulaFuncs) CEILINGdotMATH(argsList *list.List) formulaArg { // number's sign), to the nearest multiple of a given number. The syntax of // the function is: // -// CEILING.PRECISE(number,[significance]) -// +// CEILING.PRECISE(number,[significance]) func (fn *formulaFuncs) CEILINGdotPRECISE(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "CEILING.PRECISE requires at least 1 argument") @@ -3931,8 +3860,7 @@ func (fn *formulaFuncs) CEILINGdotPRECISE(argsList *list.List) formulaArg { // COMBIN function calculates the number of combinations (in any order) of a // given number objects from a set. The syntax of the function is: // -// COMBIN(number,number_chosen) -// +// COMBIN(number,number_chosen) func (fn *formulaFuncs) COMBIN(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "COMBIN requires 2 argument") @@ -3964,8 +3892,7 @@ func (fn *formulaFuncs) COMBIN(argsList *list.List) formulaArg { // COMBINA function calculates the number of combinations, with repetitions, // of a given number objects from a set. The syntax of the function is: // -// COMBINA(number,number_chosen) -// +// COMBINA(number,number_chosen) func (fn *formulaFuncs) COMBINA(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "COMBINA requires 2 argument") @@ -4003,8 +3930,7 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) formulaArg { // COS function calculates the cosine of a given angle. The syntax of the // function is: // -// COS(number) -// +// COS(number) func (fn *formulaFuncs) COS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COS requires 1 numeric argument") @@ -4019,8 +3945,7 @@ func (fn *formulaFuncs) COS(argsList *list.List) formulaArg { // COSH function calculates the hyperbolic cosine (cosh) of a supplied number. // The syntax of the function is: // -// COSH(number) -// +// COSH(number) func (fn *formulaFuncs) COSH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COSH requires 1 numeric argument") @@ -4035,8 +3960,7 @@ func (fn *formulaFuncs) COSH(argsList *list.List) formulaArg { // COT function calculates the cotangent of a given angle. The syntax of the // function is: // -// COT(number) -// +// COT(number) func (fn *formulaFuncs) COT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COT requires 1 numeric argument") @@ -4054,8 +3978,7 @@ func (fn *formulaFuncs) COT(argsList *list.List) formulaArg { // COTH function calculates the hyperbolic cotangent (coth) of a supplied // angle. The syntax of the function is: // -// COTH(number) -// +// COTH(number) func (fn *formulaFuncs) COTH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COTH requires 1 numeric argument") @@ -4073,8 +3996,7 @@ func (fn *formulaFuncs) COTH(argsList *list.List) formulaArg { // CSC function calculates the cosecant of a given angle. The syntax of the // function is: // -// CSC(number) -// +// CSC(number) func (fn *formulaFuncs) CSC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "CSC requires 1 numeric argument") @@ -4092,8 +4014,7 @@ func (fn *formulaFuncs) CSC(argsList *list.List) formulaArg { // CSCH function calculates the hyperbolic cosecant (csch) of a supplied // angle. The syntax of the function is: // -// CSCH(number) -// +// CSCH(number) func (fn *formulaFuncs) CSCH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "CSCH requires 1 numeric argument") @@ -4111,8 +4032,7 @@ func (fn *formulaFuncs) CSCH(argsList *list.List) formulaArg { // DECIMAL function converts a text representation of a number in a specified // base, into a decimal value. The syntax of the function is: // -// DECIMAL(text,radix) -// +// DECIMAL(text,radix) func (fn *formulaFuncs) DECIMAL(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "DECIMAL requires 2 numeric arguments") @@ -4136,8 +4056,7 @@ func (fn *formulaFuncs) DECIMAL(argsList *list.List) formulaArg { // DEGREES function converts radians into degrees. The syntax of the function // is: // -// DEGREES(angle) -// +// DEGREES(angle) func (fn *formulaFuncs) DEGREES(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "DEGREES requires 1 numeric argument") @@ -4156,8 +4075,7 @@ func (fn *formulaFuncs) DEGREES(argsList *list.List) formulaArg { // positive number up and a negative number down), to the next even number. // The syntax of the function is: // -// EVEN(number) -// +// EVEN(number) func (fn *formulaFuncs) EVEN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "EVEN requires 1 numeric argument") @@ -4182,8 +4100,7 @@ func (fn *formulaFuncs) EVEN(argsList *list.List) formulaArg { // EXP function calculates the value of the mathematical constant e, raised to // the power of a given number. The syntax of the function is: // -// EXP(number) -// +// EXP(number) func (fn *formulaFuncs) EXP(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "EXP requires 1 numeric argument") @@ -4207,8 +4124,7 @@ func fact(number float64) float64 { // FACT function returns the factorial of a supplied number. The syntax of the // function is: // -// FACT(number) -// +// FACT(number) func (fn *formulaFuncs) FACT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "FACT requires 1 numeric argument") @@ -4226,8 +4142,7 @@ func (fn *formulaFuncs) FACT(argsList *list.List) formulaArg { // FACTDOUBLE function returns the double factorial of a supplied number. The // syntax of the function is: // -// FACTDOUBLE(number) -// +// FACTDOUBLE(number) func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "FACTDOUBLE requires 1 numeric argument") @@ -4249,8 +4164,7 @@ func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) formulaArg { // FLOOR function rounds a supplied number towards zero to the nearest // multiple of a specified significance. The syntax of the function is: // -// FLOOR(number,significance) -// +// FLOOR(number,significance) func (fn *formulaFuncs) FLOOR(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "FLOOR requires 2 numeric arguments") @@ -4279,8 +4193,7 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) formulaArg { // FLOORdotMATH function rounds a supplied number down to a supplied multiple // of significance. The syntax of the function is: // -// FLOOR.MATH(number,[significance],[mode]) -// +// FLOOR.MATH(number,[significance],[mode]) func (fn *formulaFuncs) FLOORdotMATH(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.MATH requires at least 1 argument") @@ -4323,8 +4236,7 @@ func (fn *formulaFuncs) FLOORdotMATH(argsList *list.List) formulaArg { // FLOORdotPRECISE function rounds a supplied number down to a supplied // multiple of significance. The syntax of the function is: // -// FLOOR.PRECISE(number,[significance]) -// +// FLOOR.PRECISE(number,[significance]) func (fn *formulaFuncs) FLOORdotPRECISE(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.PRECISE requires at least 1 argument") @@ -4385,8 +4297,7 @@ func gcd(x, y float64) float64 { // GCD function returns the greatest common divisor of two or more supplied // integers. The syntax of the function is: // -// GCD(number1,[number2],...) -// +// GCD(number1,[number2],...) func (fn *formulaFuncs) GCD(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "GCD requires at least 1 argument") @@ -4428,8 +4339,7 @@ func (fn *formulaFuncs) GCD(argsList *list.List) formulaArg { // INT function truncates a supplied number down to the closest integer. The // syntax of the function is: // -// INT(number) -// +// INT(number) func (fn *formulaFuncs) INT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "INT requires 1 numeric argument") @@ -4449,8 +4359,7 @@ func (fn *formulaFuncs) INT(argsList *list.List) formulaArg { // number's sign), to the nearest multiple of a supplied significance. The // syntax of the function is: // -// ISO.CEILING(number,[significance]) -// +// ISO.CEILING(number,[significance]) func (fn *formulaFuncs) ISOdotCEILING(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "ISO.CEILING requires at least 1 argument") @@ -4502,8 +4411,7 @@ func lcm(a, b float64) float64 { // LCM function returns the least common multiple of two or more supplied // integers. The syntax of the function is: // -// LCM(number1,[number2],...) -// +// LCM(number1,[number2],...) func (fn *formulaFuncs) LCM(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "LCM requires at least 1 argument") @@ -4547,8 +4455,7 @@ func (fn *formulaFuncs) LCM(argsList *list.List) formulaArg { // LN function calculates the natural logarithm of a given number. The syntax // of the function is: // -// LN(number) -// +// LN(number) func (fn *formulaFuncs) LN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LN requires 1 numeric argument") @@ -4563,8 +4470,7 @@ func (fn *formulaFuncs) LN(argsList *list.List) formulaArg { // LOG function calculates the logarithm of a given number, to a supplied // base. The syntax of the function is: // -// LOG(number,[base]) -// +// LOG(number,[base]) func (fn *formulaFuncs) LOG(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "LOG requires at least 1 argument") @@ -4599,8 +4505,7 @@ func (fn *formulaFuncs) LOG(argsList *list.List) formulaArg { // LOG10 function calculates the base 10 logarithm of a given number. The // syntax of the function is: // -// LOG10(number) -// +// LOG10(number) func (fn *formulaFuncs) LOG10(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LOG10 requires 1 numeric argument") @@ -4683,8 +4588,7 @@ func newFormulaArgMatrix(numMtx [][]float64) (arg [][]formulaArg) { // MDETERM calculates the determinant of a square matrix. The // syntax of the function is: // -// MDETERM(array) -// +// MDETERM(array) func (fn *formulaFuncs) MDETERM(argsList *list.List) (result formulaArg) { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "MDETERM requires 1 argument") @@ -4745,8 +4649,7 @@ func adjugateMatrix(A [][]float64) (adjA [][]float64) { // MINVERSE function calculates the inverse of a square matrix. The syntax of // the function is: // -// MINVERSE(array) -// +// MINVERSE(array) func (fn *formulaFuncs) MINVERSE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "MINVERSE requires 1 argument") @@ -4770,8 +4673,7 @@ func (fn *formulaFuncs) MINVERSE(argsList *list.List) formulaArg { // MMULT function calculates the matrix product of two arrays // (representing matrices). The syntax of the function is: // -// MMULT(array1,array2) -// +// MMULT(array1,array2) func (fn *formulaFuncs) MMULT(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "MMULT requires 2 argument") @@ -4813,8 +4715,7 @@ func (fn *formulaFuncs) MMULT(argsList *list.List) formulaArg { // MOD function returns the remainder of a division between two supplied // numbers. The syntax of the function is: // -// MOD(number,divisor) -// +// MOD(number,divisor) func (fn *formulaFuncs) MOD(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "MOD requires 2 numeric arguments") @@ -4840,8 +4741,7 @@ func (fn *formulaFuncs) MOD(argsList *list.List) formulaArg { // MROUND function rounds a supplied number up or down to the nearest multiple // of a given number. The syntax of the function is: // -// MROUND(number,multiple) -// +// MROUND(number,multiple) func (fn *formulaFuncs) MROUND(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "MROUND requires 2 numeric arguments") @@ -4872,8 +4772,7 @@ func (fn *formulaFuncs) MROUND(argsList *list.List) formulaArg { // supplied values to the product of factorials of those values. The syntax of // the function is: // -// MULTINOMIAL(number1,[number2],...) -// +// MULTINOMIAL(number1,[number2],...) func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) formulaArg { val, num, denom := 0.0, 0.0, 1.0 var err error @@ -4899,8 +4798,7 @@ func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) formulaArg { // MUNIT function returns the unit matrix for a specified dimension. The // syntax of the function is: // -// MUNIT(dimension) -// +// MUNIT(dimension) func (fn *formulaFuncs) MUNIT(argsList *list.List) (result formulaArg) { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "MUNIT requires 1 numeric argument") @@ -4928,8 +4826,7 @@ func (fn *formulaFuncs) MUNIT(argsList *list.List) (result formulaArg) { // number up and a negative number down), to the next odd number. The syntax // of the function is: // -// ODD(number) -// +// ODD(number) func (fn *formulaFuncs) ODD(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ODD requires 1 numeric argument") @@ -4957,8 +4854,7 @@ func (fn *formulaFuncs) ODD(argsList *list.List) formulaArg { // PI function returns the value of the mathematical constant π (pi), accurate // to 15 digits (14 decimal places). The syntax of the function is: // -// PI() -// +// PI() func (fn *formulaFuncs) PI(argsList *list.List) formulaArg { if argsList.Len() != 0 { return newErrorFormulaArg(formulaErrorVALUE, "PI accepts no arguments") @@ -4969,8 +4865,7 @@ func (fn *formulaFuncs) PI(argsList *list.List) formulaArg { // POWER function calculates a given number, raised to a supplied power. // The syntax of the function is: // -// POWER(number,power) -// +// POWER(number,power) func (fn *formulaFuncs) POWER(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "POWER requires 2 numeric arguments") @@ -4995,8 +4890,7 @@ func (fn *formulaFuncs) POWER(argsList *list.List) formulaArg { // PRODUCT function returns the product (multiplication) of a supplied set of // numerical values. The syntax of the function is: // -// PRODUCT(number1,[number2],...) -// +// PRODUCT(number1,[number2],...) func (fn *formulaFuncs) PRODUCT(argsList *list.List) formulaArg { val, product := 0.0, 1.0 var err error @@ -5033,8 +4927,7 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) formulaArg { // QUOTIENT function returns the integer portion of a division between two // supplied numbers. The syntax of the function is: // -// QUOTIENT(numerator,denominator) -// +// QUOTIENT(numerator,denominator) func (fn *formulaFuncs) QUOTIENT(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "QUOTIENT requires 2 numeric arguments") @@ -5055,8 +4948,7 @@ func (fn *formulaFuncs) QUOTIENT(argsList *list.List) formulaArg { // RADIANS function converts radians into degrees. The syntax of the function is: // -// RADIANS(angle) -// +// RADIANS(angle) func (fn *formulaFuncs) RADIANS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "RADIANS requires 1 numeric argument") @@ -5071,8 +4963,7 @@ func (fn *formulaFuncs) RADIANS(argsList *list.List) formulaArg { // RAND function generates a random real number between 0 and 1. The syntax of // the function is: // -// RAND() -// +// RAND() func (fn *formulaFuncs) RAND(argsList *list.List) formulaArg { if argsList.Len() != 0 { return newErrorFormulaArg(formulaErrorVALUE, "RAND accepts no arguments") @@ -5083,8 +4974,7 @@ func (fn *formulaFuncs) RAND(argsList *list.List) formulaArg { // RANDBETWEEN function generates a random integer between two supplied // integers. The syntax of the function is: // -// RANDBETWEEN(bottom,top) -// +// RANDBETWEEN(bottom,top) func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "RANDBETWEEN requires 2 numeric arguments") @@ -5222,8 +5112,7 @@ var romanTable = [][]romanNumerals{ // integer, the function returns a text string depicting the roman numeral // form of the number. The syntax of the function is: // -// ROMAN(number,[form]) -// +// ROMAN(number,[form]) func (fn *formulaFuncs) ROMAN(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "ROMAN requires at least 1 argument") @@ -5309,8 +5198,7 @@ func (fn *formulaFuncs) round(number, digits float64, mode roundMode) float64 { // ROUND function rounds a supplied number up or down, to a specified number // of decimal places. The syntax of the function is: // -// ROUND(number,num_digits) -// +// ROUND(number,num_digits) func (fn *formulaFuncs) ROUND(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "ROUND requires 2 numeric arguments") @@ -5329,8 +5217,7 @@ func (fn *formulaFuncs) ROUND(argsList *list.List) formulaArg { // ROUNDDOWN function rounds a supplied number down towards zero, to a // specified number of decimal places. The syntax of the function is: // -// ROUNDDOWN(number,num_digits) -// +// ROUNDDOWN(number,num_digits) func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "ROUNDDOWN requires 2 numeric arguments") @@ -5349,8 +5236,7 @@ func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) formulaArg { // ROUNDUP function rounds a supplied number up, away from zero, to a // specified number of decimal places. The syntax of the function is: // -// ROUNDUP(number,num_digits) -// +// ROUNDUP(number,num_digits) func (fn *formulaFuncs) ROUNDUP(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "ROUNDUP requires 2 numeric arguments") @@ -5369,8 +5255,7 @@ func (fn *formulaFuncs) ROUNDUP(argsList *list.List) formulaArg { // SEC function calculates the secant of a given angle. The syntax of the // function is: // -// SEC(number) -// +// SEC(number) func (fn *formulaFuncs) SEC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SEC requires 1 numeric argument") @@ -5385,8 +5270,7 @@ func (fn *formulaFuncs) SEC(argsList *list.List) formulaArg { // SECH function calculates the hyperbolic secant (sech) of a supplied angle. // The syntax of the function is: // -// SECH(number) -// +// SECH(number) func (fn *formulaFuncs) SECH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SECH requires 1 numeric argument") @@ -5401,8 +5285,7 @@ func (fn *formulaFuncs) SECH(argsList *list.List) formulaArg { // SERIESSUM function returns the sum of a power series. The syntax of the // function is: // -// SERIESSUM(x,n,m,coefficients) -// +// SERIESSUM(x,n,m,coefficients) func (fn *formulaFuncs) SERIESSUM(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "SERIESSUM requires 4 arguments") @@ -5437,8 +5320,7 @@ func (fn *formulaFuncs) SERIESSUM(argsList *list.List) formulaArg { // the number is negative, the function returns -1 and if the number is 0 // (zero), the function returns 0. The syntax of the function is: // -// SIGN(number) -// +// SIGN(number) func (fn *formulaFuncs) SIGN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SIGN requires 1 numeric argument") @@ -5459,8 +5341,7 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) formulaArg { // SIN function calculates the sine of a given angle. The syntax of the // function is: // -// SIN(number) -// +// SIN(number) func (fn *formulaFuncs) SIN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SIN requires 1 numeric argument") @@ -5475,8 +5356,7 @@ func (fn *formulaFuncs) SIN(argsList *list.List) formulaArg { // SINH function calculates the hyperbolic sine (sinh) of a supplied number. // The syntax of the function is: // -// SINH(number) -// +// SINH(number) func (fn *formulaFuncs) SINH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SINH requires 1 numeric argument") @@ -5491,8 +5371,7 @@ func (fn *formulaFuncs) SINH(argsList *list.List) formulaArg { // SQRT function calculates the positive square root of a supplied number. The // syntax of the function is: // -// SQRT(number) -// +// SQRT(number) func (fn *formulaFuncs) SQRT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SQRT requires 1 numeric argument") @@ -5510,8 +5389,7 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) formulaArg { // SQRTPI function returns the square root of a supplied number multiplied by // the mathematical constant, π. The syntax of the function is: // -// SQRTPI(number) -// +// SQRTPI(number) func (fn *formulaFuncs) SQRTPI(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SQRTPI requires 1 numeric argument") @@ -5526,8 +5404,7 @@ func (fn *formulaFuncs) SQRTPI(argsList *list.List) formulaArg { // STDEV function calculates the sample standard deviation of a supplied set // of values. The syntax of the function is: // -// STDEV(number1,[number2],...) -// +// STDEV(number1,[number2],...) func (fn *formulaFuncs) STDEV(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "STDEV requires at least 1 argument") @@ -5538,8 +5415,7 @@ func (fn *formulaFuncs) STDEV(argsList *list.List) formulaArg { // STDEVdotS function calculates the sample standard deviation of a supplied // set of values. The syntax of the function is: // -// STDEV.S(number1,[number2],...) -// +// STDEV.S(number1,[number2],...) func (fn *formulaFuncs) STDEVdotS(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "STDEV.S requires at least 1 argument") @@ -5551,8 +5427,7 @@ func (fn *formulaFuncs) STDEVdotS(argsList *list.List) formulaArg { // standard deviation is a measure of how widely values are dispersed from // the average value (the mean). The syntax of the function is: // -// STDEVA(number1,[number2],...) -// +// STDEVA(number1,[number2],...) func (fn *formulaFuncs) STDEVA(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "STDEVA requires at least 1 argument") @@ -5635,8 +5510,7 @@ func (fn *formulaFuncs) stdev(stdeva bool, argsList *list.List) formulaArg { // the Cumulative Poisson Probability Function for a supplied set of // parameters. The syntax of the function is: // -// POISSON.DIST(x,mean,cumulative) -// +// POISSON.DIST(x,mean,cumulative) func (fn *formulaFuncs) POISSONdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "POISSON.DIST requires 3 arguments") @@ -5648,8 +5522,7 @@ func (fn *formulaFuncs) POISSONdotDIST(argsList *list.List) formulaArg { // Cumulative Poisson Probability Function for a supplied set of parameters. // The syntax of the function is: // -// POISSON(x,mean,cumulative) -// +// POISSON(x,mean,cumulative) func (fn *formulaFuncs) POISSON(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "POISSON requires 3 arguments") @@ -5681,8 +5554,7 @@ func (fn *formulaFuncs) POISSON(argsList *list.List) formulaArg { // SUM function adds together a supplied set of numbers and returns the sum of // these values. The syntax of the function is: // -// SUM(number1,[number2],...) -// +// SUM(number1,[number2],...) func (fn *formulaFuncs) SUM(argsList *list.List) formulaArg { var sum float64 for arg := argsList.Front(); arg != nil; arg = arg.Next() { @@ -5713,8 +5585,7 @@ func (fn *formulaFuncs) SUM(argsList *list.List) formulaArg { // criteria, and returns the sum of the corresponding values in a second // supplied array. The syntax of the function is: // -// SUMIF(range,criteria,[sum_range]) -// +// SUMIF(range,criteria,[sum_range]) func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "SUMIF requires at least 2 arguments") @@ -5755,8 +5626,7 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { // set of criteria, and returns the sum of the corresponding values in a // further supplied array. The syntax of the function is: // -// SUMIFS(sum_range,criteria_range1,criteria1,[criteria_range2,criteria2],...) -// +// SUMIFS(sum_range,criteria_range1,criteria1,[criteria_range2,criteria2],...) func (fn *formulaFuncs) SUMIFS(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "SUMIFS requires at least 3 arguments") @@ -5833,8 +5703,7 @@ func (fn *formulaFuncs) sumproduct(argsList *list.List) formulaArg { // SUMPRODUCT function returns the sum of the products of the corresponding // values in a set of supplied arrays. The syntax of the function is: // -// SUMPRODUCT(array1,[array2],[array3],...) -// +// SUMPRODUCT(array1,[array2],[array3],...) func (fn *formulaFuncs) SUMPRODUCT(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "SUMPRODUCT requires at least 1 argument") @@ -5850,8 +5719,7 @@ func (fn *formulaFuncs) SUMPRODUCT(argsList *list.List) formulaArg { // SUMSQ function returns the sum of squares of a supplied set of values. The // syntax of the function is: // -// SUMSQ(number1,[number2],...) -// +// SUMSQ(number1,[number2],...) func (fn *formulaFuncs) SUMSQ(argsList *list.List) formulaArg { var val, sq float64 var err error @@ -5917,8 +5785,7 @@ func (fn *formulaFuncs) sumx(name string, argsList *list.List) formulaArg { // SUMX2MY2 function returns the sum of the differences of squares of two // supplied sets of values. The syntax of the function is: // -// SUMX2MY2(array_x,array_y) -// +// SUMX2MY2(array_x,array_y) func (fn *formulaFuncs) SUMX2MY2(argsList *list.List) formulaArg { return fn.sumx("SUMX2MY2", argsList) } @@ -5926,8 +5793,7 @@ func (fn *formulaFuncs) SUMX2MY2(argsList *list.List) formulaArg { // SUMX2PY2 function returns the sum of the sum of squares of two supplied sets // of values. The syntax of the function is: // -// SUMX2PY2(array_x,array_y) -// +// SUMX2PY2(array_x,array_y) func (fn *formulaFuncs) SUMX2PY2(argsList *list.List) formulaArg { return fn.sumx("SUMX2PY2", argsList) } @@ -5936,8 +5802,7 @@ func (fn *formulaFuncs) SUMX2PY2(argsList *list.List) formulaArg { // corresponding values in two supplied arrays. The syntax of the function // is: // -// SUMXMY2(array_x,array_y) -// +// SUMXMY2(array_x,array_y) func (fn *formulaFuncs) SUMXMY2(argsList *list.List) formulaArg { return fn.sumx("SUMXMY2", argsList) } @@ -5945,8 +5810,7 @@ func (fn *formulaFuncs) SUMXMY2(argsList *list.List) formulaArg { // TAN function calculates the tangent of a given angle. The syntax of the // function is: // -// TAN(number) -// +// TAN(number) func (fn *formulaFuncs) TAN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "TAN requires 1 numeric argument") @@ -5961,8 +5825,7 @@ func (fn *formulaFuncs) TAN(argsList *list.List) formulaArg { // TANH function calculates the hyperbolic tangent (tanh) of a supplied // number. The syntax of the function is: // -// TANH(number) -// +// TANH(number) func (fn *formulaFuncs) TANH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "TANH requires 1 numeric argument") @@ -5977,8 +5840,7 @@ func (fn *formulaFuncs) TANH(argsList *list.List) formulaArg { // TRUNC function truncates a supplied number to a specified number of decimal // places. The syntax of the function is: // -// TRUNC(number,[number_digits]) -// +// TRUNC(number,[number_digits]) func (fn *formulaFuncs) TRUNC(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "TRUNC requires at least 1 argument") @@ -6015,8 +5877,7 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) formulaArg { // AVEDEV function calculates the average deviation of a supplied set of // values. The syntax of the function is: // -// AVEDEV(number1,[number2],...) -// +// AVEDEV(number1,[number2],...) func (fn *formulaFuncs) AVEDEV(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "AVEDEV requires at least 1 argument") @@ -6040,8 +5901,7 @@ func (fn *formulaFuncs) AVEDEV(argsList *list.List) formulaArg { // AVERAGE function returns the arithmetic mean of a list of supplied numbers. // The syntax of the function is: // -// AVERAGE(number1,[number2],...) -// +// AVERAGE(number1,[number2],...) func (fn *formulaFuncs) AVERAGE(argsList *list.List) formulaArg { var args []formulaArg for arg := argsList.Front(); arg != nil; arg = arg.Next() { @@ -6057,8 +5917,7 @@ func (fn *formulaFuncs) AVERAGE(argsList *list.List) formulaArg { // AVERAGEA function returns the arithmetic mean of a list of supplied numbers // with text cell and zero values. The syntax of the function is: // -// AVERAGEA(number1,[number2],...) -// +// AVERAGEA(number1,[number2],...) func (fn *formulaFuncs) AVERAGEA(argsList *list.List) formulaArg { var args []formulaArg for arg := argsList.Front(); arg != nil; arg = arg.Next() { @@ -6076,8 +5935,7 @@ func (fn *formulaFuncs) AVERAGEA(argsList *list.List) formulaArg { // the corresponding values in a second supplied array. The syntax of the // function is: // -// AVERAGEIF(range,criteria,[average_range]) -// +// AVERAGEIF(range,criteria,[average_range]) func (fn *formulaFuncs) AVERAGEIF(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "AVERAGEIF requires at least 2 arguments") @@ -6126,8 +5984,7 @@ func (fn *formulaFuncs) AVERAGEIF(argsList *list.List) formulaArg { // of the corresponding values in a further supplied array. The syntax of the // function is: // -// AVERAGEIFS(average_range,criteria_range1,criteria1,[criteria_range2,criteria2],...) -// +// AVERAGEIFS(average_range,criteria_range1,criteria1,[criteria_range2,criteria2],...) func (fn *formulaFuncs) AVERAGEIFS(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "AVERAGEIFS requires at least 3 arguments") @@ -6403,8 +6260,7 @@ func (fn *formulaFuncs) prepareBETAdotDISTArgs(argsList *list.List) formulaArg { // or the probability density function of the Beta distribution, for a // supplied set of parameters. The syntax of the function is: // -// BETA.DIST(x,alpha,beta,cumulative,[A],[B]) -// +// BETA.DIST(x,alpha,beta,cumulative,[A],[B]) func (fn *formulaFuncs) BETAdotDIST(argsList *list.List) formulaArg { args := fn.prepareBETAdotDISTArgs(argsList) if args.Type != ArgList { @@ -6428,8 +6284,7 @@ func (fn *formulaFuncs) BETAdotDIST(argsList *list.List) formulaArg { // BETADIST function calculates the cumulative beta probability density // function for a supplied set of parameters. The syntax of the function is: // -// BETADIST(x,alpha,beta,[A],[B]) -// +// BETADIST(x,alpha,beta,[A],[B]) func (fn *formulaFuncs) BETADIST(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "BETADIST requires at least 3 arguments") @@ -6826,8 +6681,7 @@ func (fn *formulaFuncs) betainv(name string, argsList *list.List) formulaArg { // the cumulative beta probability density function for a supplied // probability. The syntax of the function is: // -// BETAINV(probability,alpha,beta,[A],[B]) -// +// BETAINV(probability,alpha,beta,[A],[B]) func (fn *formulaFuncs) BETAINV(argsList *list.List) formulaArg { return fn.betainv("BETAINV", argsList) } @@ -6836,8 +6690,7 @@ func (fn *formulaFuncs) BETAINV(argsList *list.List) formulaArg { // the cumulative beta probability density function for a supplied // probability. The syntax of the function is: // -// BETA.INV(probability,alpha,beta,[A],[B]) -// +// BETA.INV(probability,alpha,beta,[A],[B]) func (fn *formulaFuncs) BETAdotINV(argsList *list.List) formulaArg { return fn.betainv("BETA.INV", argsList) } @@ -6870,8 +6723,7 @@ func binomdist(x, n, p float64) float64 { // given number of successes from a specified number of trials. The syntax of // the function is: // -// BINOM.DIST(number_s,trials,probability_s,cumulative) -// +// BINOM.DIST(number_s,trials,probability_s,cumulative) func (fn *formulaFuncs) BINOMdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "BINOM.DIST requires 4 arguments") @@ -6883,8 +6735,7 @@ func (fn *formulaFuncs) BINOMdotDIST(argsList *list.List) formulaArg { // specified number of successes out of a specified number of trials. The // syntax of the function is: // -// BINOMDIST(number_s,trials,probability_s,cumulative) -// +// BINOMDIST(number_s,trials,probability_s,cumulative) func (fn *formulaFuncs) BINOMDIST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "BINOMDIST requires 4 arguments") @@ -6923,8 +6774,7 @@ func (fn *formulaFuncs) BINOMDIST(argsList *list.List) formulaArg { // for the number of successes from a specified number of trials falling into // a specified range. // -// BINOM.DIST.RANGE(trials,probability_s,number_s,[number_s2]) -// +// BINOM.DIST.RANGE(trials,probability_s,number_s,[number_s2]) func (fn *formulaFuncs) BINOMdotDISTdotRANGE(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "BINOM.DIST.RANGE requires at least 3 arguments") @@ -6991,8 +6841,7 @@ func binominv(n, p, alpha float64) float64 { // BINOMdotINV function returns the inverse of the Cumulative Binomial // Distribution. The syntax of the function is: // -// BINOM.INV(trials,probability_s,alpha) -// +// BINOM.INV(trials,probability_s,alpha) func (fn *formulaFuncs) BINOMdotINV(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "BINOM.INV requires 3 numeric arguments") @@ -7024,8 +6873,7 @@ func (fn *formulaFuncs) BINOMdotINV(argsList *list.List) formulaArg { // CHIDIST function calculates the right-tailed probability of the chi-square // distribution. The syntax of the function is: // -// CHIDIST(x,degrees_freedom) -// +// CHIDIST(x,degrees_freedom) func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "CHIDIST requires 2 numeric arguments") @@ -7088,8 +6936,7 @@ func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { // CHIINV function calculates the inverse of the right-tailed probability of // the Chi-Square Distribution. The syntax of the function is: // -// CHIINV(probability,deg_freedom) -// +// CHIINV(probability,deg_freedom) func (fn *formulaFuncs) CHIINV(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "CHIINV requires 2 numeric arguments") @@ -7116,8 +6963,7 @@ func (fn *formulaFuncs) CHIINV(argsList *list.List) formulaArg { // frequencies), are likely to be simply due to sampling error, or if they are // likely to be real. The syntax of the function is: // -// CHITEST(actual_range,expected_range) -// +// CHITEST(actual_range,expected_range) func (fn *formulaFuncs) CHITEST(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "CHITEST requires 2 arguments") @@ -7309,8 +7155,7 @@ func getChiSqDistPDF(fX, fDF float64) float64 { // Cumulative Distribution Function for the Chi-Square Distribution. The // syntax of the function is: // -// CHISQ.DIST(x,degrees_freedom,cumulative) -// +// CHISQ.DIST(x,degrees_freedom,cumulative) func (fn *formulaFuncs) CHISQdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.DIST requires 3 arguments") @@ -7341,8 +7186,7 @@ func (fn *formulaFuncs) CHISQdotDIST(argsList *list.List) formulaArg { // CHISQdotDISTdotRT function calculates the right-tailed probability of the // Chi-Square Distribution. The syntax of the function is: // -// CHISQ.DIST.RT(x,degrees_freedom) -// +// CHISQ.DIST.RT(x,degrees_freedom) func (fn *formulaFuncs) CHISQdotDISTdotRT(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.DIST.RT requires 2 numeric arguments") @@ -7355,8 +7199,7 @@ func (fn *formulaFuncs) CHISQdotDISTdotRT(argsList *list.List) formulaArg { // the differences between the sets are simply due to sampling error. The // syntax of the function is: // -// CHISQ.TEST(actual_range,expected_range) -// +// CHISQ.TEST(actual_range,expected_range) func (fn *formulaFuncs) CHISQdotTEST(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.TEST requires 2 arguments") @@ -7454,8 +7297,7 @@ func calcIterateInverse(iterator calcInverseIterator, fAx, fBx float64) float64 // CHISQdotINV function calculates the inverse of the left-tailed probability // of the Chi-Square Distribution. The syntax of the function is: // -// CHISQ.INV(probability,degrees_freedom) -// +// CHISQ.INV(probability,degrees_freedom) func (fn *formulaFuncs) CHISQdotINV(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.INV requires 2 numeric arguments") @@ -7483,8 +7325,7 @@ func (fn *formulaFuncs) CHISQdotINV(argsList *list.List) formulaArg { // CHISQdotINVdotRT function calculates the inverse of the right-tailed // probability of the Chi-Square Distribution. The syntax of the function is: // -// CHISQ.INV.RT(probability,degrees_freedom) -// +// CHISQ.INV.RT(probability,degrees_freedom) func (fn *formulaFuncs) CHISQdotINVdotRT(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.INV.RT requires 2 numeric arguments") @@ -7533,8 +7374,7 @@ func (fn *formulaFuncs) confidence(name string, argsList *list.List) formulaArg // that the standard deviation of the population is known. The syntax of the // function is: // -// CONFIDENCE(alpha,standard_dev,size) -// +// CONFIDENCE(alpha,standard_dev,size) func (fn *formulaFuncs) CONFIDENCE(argsList *list.List) formulaArg { return fn.confidence("CONFIDENCE", argsList) } @@ -7545,8 +7385,7 @@ func (fn *formulaFuncs) CONFIDENCE(argsList *list.List) formulaArg { // assumed that the standard deviation of the population is known. The syntax // of the function is: // -// CONFIDENCE.NORM(alpha,standard_dev,size) -// +// CONFIDENCE.NORM(alpha,standard_dev,size) func (fn *formulaFuncs) CONFIDENCEdotNORM(argsList *list.List) formulaArg { return fn.confidence("CONFIDENCE.NORM", argsList) } @@ -7557,8 +7396,7 @@ func (fn *formulaFuncs) CONFIDENCEdotNORM(argsList *list.List) formulaArg { // is assumed that the standard deviation of the population is known. The // syntax of the function is: // -// CONFIDENCE.T(alpha,standard_dev,size) -// +// CONFIDENCE.T(alpha,standard_dev,size) func (fn *formulaFuncs) CONFIDENCEdotT(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "CONFIDENCE.T requires 3 arguments") @@ -7623,8 +7461,7 @@ func (fn *formulaFuncs) covar(name string, argsList *list.List) formulaArg { // COVAR function calculates the covariance of two supplied sets of values. The // syntax of the function is: // -// COVAR(array1,array2) -// +// COVAR(array1,array2) func (fn *formulaFuncs) COVAR(argsList *list.List) formulaArg { return fn.covar("COVAR", argsList) } @@ -7632,8 +7469,7 @@ func (fn *formulaFuncs) COVAR(argsList *list.List) formulaArg { // COVARIANCEdotP function calculates the population covariance of two supplied // sets of values. The syntax of the function is: // -// COVARIANCE.P(array1,array2) -// +// COVARIANCE.P(array1,array2) func (fn *formulaFuncs) COVARIANCEdotP(argsList *list.List) formulaArg { return fn.covar("COVARIANCE.P", argsList) } @@ -7641,8 +7477,7 @@ func (fn *formulaFuncs) COVARIANCEdotP(argsList *list.List) formulaArg { // COVARIANCEdotS function calculates the sample covariance of two supplied // sets of values. The syntax of the function is: // -// COVARIANCE.S(array1,array2) -// +// COVARIANCE.S(array1,array2) func (fn *formulaFuncs) COVARIANCEdotS(argsList *list.List) formulaArg { return fn.covar("COVARIANCE.S", argsList) } @@ -7693,8 +7528,7 @@ func (fn *formulaFuncs) countSum(countText bool, args []formulaArg) (count, sum // CORREL function calculates the Pearson Product-Moment Correlation // Coefficient for two sets of values. The syntax of the function is: // -// CORREL(array1,array2) -// +// CORREL(array1,array2) func (fn *formulaFuncs) CORREL(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "CORREL requires 2 arguments") @@ -7733,8 +7567,7 @@ func (fn *formulaFuncs) CORREL(argsList *list.List) formulaArg { // cells or values. This count includes both numbers and dates. The syntax of // the function is: // -// COUNT(value1,[value2],...) -// +// COUNT(value1,[value2],...) func (fn *formulaFuncs) COUNT(argsList *list.List) formulaArg { var count int for token := argsList.Front(); token != nil; token = token.Next() { @@ -7760,8 +7593,7 @@ func (fn *formulaFuncs) COUNT(argsList *list.List) formulaArg { // COUNTA function returns the number of non-blanks within a supplied set of // cells or values. The syntax of the function is: // -// COUNTA(value1,[value2],...) -// +// COUNTA(value1,[value2],...) func (fn *formulaFuncs) COUNTA(argsList *list.List) formulaArg { var count int for token := argsList.Front(); token != nil; token = token.Next() { @@ -7792,8 +7624,7 @@ func (fn *formulaFuncs) COUNTA(argsList *list.List) formulaArg { // COUNTBLANK function returns the number of blank cells in a supplied range. // The syntax of the function is: // -// COUNTBLANK(range) -// +// COUNTBLANK(range) func (fn *formulaFuncs) COUNTBLANK(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COUNTBLANK requires 1 argument") @@ -7810,8 +7641,7 @@ func (fn *formulaFuncs) COUNTBLANK(argsList *list.List) formulaArg { // COUNTIF function returns the number of cells within a supplied range, that // satisfy a given criteria. The syntax of the function is: // -// COUNTIF(range,criteria) -// +// COUNTIF(range,criteria) func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "COUNTIF requires 2 arguments") @@ -7860,8 +7690,7 @@ func formulaIfsMatch(args []formulaArg) (cellRefs []cellRef) { // COUNTIFS function returns the number of rows within a table, that satisfy a // set of given criteria. The syntax of the function is: // -// COUNTIFS(criteria_range1,criteria1,[criteria_range2,criteria2],...) -// +// COUNTIFS(criteria_range1,criteria1,[criteria_range2,criteria2],...) func (fn *formulaFuncs) COUNTIFS(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "COUNTIFS requires at least 2 arguments") @@ -7882,8 +7711,7 @@ func (fn *formulaFuncs) COUNTIFS(argsList *list.List) formulaArg { // cumulative binomial distribution is greater than or equal to a specified // value. The syntax of the function is: // -// CRITBINOM(trials,probability_s,alpha) -// +// CRITBINOM(trials,probability_s,alpha) func (fn *formulaFuncs) CRITBINOM(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "CRITBINOM requires 3 numeric arguments") @@ -7894,8 +7722,7 @@ func (fn *formulaFuncs) CRITBINOM(argsList *list.List) formulaArg { // DEVSQ function calculates the sum of the squared deviations from the sample // mean. The syntax of the function is: // -// DEVSQ(number1,[number2],...) -// +// DEVSQ(number1,[number2],...) func (fn *formulaFuncs) DEVSQ(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "DEVSQ requires at least 1 numeric argument") @@ -7924,8 +7751,7 @@ func (fn *formulaFuncs) DEVSQ(argsList *list.List) formulaArg { // FISHER function calculates the Fisher Transformation for a supplied value. // The syntax of the function is: // -// FISHER(x) -// +// FISHER(x) func (fn *formulaFuncs) FISHER(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "FISHER requires 1 numeric argument") @@ -7952,8 +7778,7 @@ func (fn *formulaFuncs) FISHER(argsList *list.List) formulaArg { // FISHERINV function calculates the inverse of the Fisher Transformation and // returns a value between -1 and +1. The syntax of the function is: // -// FISHERINV(y) -// +// FISHERINV(y) func (fn *formulaFuncs) FISHERINV(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "FISHERINV requires 1 numeric argument") @@ -7974,8 +7799,7 @@ func (fn *formulaFuncs) FISHERINV(argsList *list.List) formulaArg { // GAMMA function returns the value of the Gamma Function, Γ(n), for a // specified number, n. The syntax of the function is: // -// GAMMA(number) -// +// GAMMA(number) func (fn *formulaFuncs) GAMMA(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "GAMMA requires 1 numeric argument") @@ -7994,8 +7818,7 @@ func (fn *formulaFuncs) GAMMA(argsList *list.List) formulaArg { // used to provide probabilities for values that may have a skewed // distribution, such as queuing analysis. // -// GAMMA.DIST(x,alpha,beta,cumulative) -// +// GAMMA.DIST(x,alpha,beta,cumulative) func (fn *formulaFuncs) GAMMAdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "GAMMA.DIST requires 4 arguments") @@ -8007,8 +7830,7 @@ func (fn *formulaFuncs) GAMMAdotDIST(argsList *list.List) formulaArg { // to provide probabilities for values that may have a skewed distribution, // such as queuing analysis. // -// GAMMADIST(x,alpha,beta,cumulative) -// +// GAMMADIST(x,alpha,beta,cumulative) func (fn *formulaFuncs) GAMMADIST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "GAMMADIST requires 4 arguments") @@ -8070,8 +7892,7 @@ func gammainv(probability, alpha, beta float64) float64 { // GAMMAdotINV function returns the inverse of the Gamma Cumulative // Distribution. The syntax of the function is: // -// GAMMA.INV(probability,alpha,beta) -// +// GAMMA.INV(probability,alpha,beta) func (fn *formulaFuncs) GAMMAdotINV(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "GAMMA.INV requires 3 arguments") @@ -8082,8 +7903,7 @@ func (fn *formulaFuncs) GAMMAdotINV(argsList *list.List) formulaArg { // GAMMAINV function returns the inverse of the Gamma Cumulative Distribution. // The syntax of the function is: // -// GAMMAINV(probability,alpha,beta) -// +// GAMMAINV(probability,alpha,beta) func (fn *formulaFuncs) GAMMAINV(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "GAMMAINV requires 3 arguments") @@ -8110,8 +7930,7 @@ func (fn *formulaFuncs) GAMMAINV(argsList *list.List) formulaArg { // GAMMALN function returns the natural logarithm of the Gamma Function, Γ // (n). The syntax of the function is: // -// GAMMALN(x) -// +// GAMMALN(x) func (fn *formulaFuncs) GAMMALN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") @@ -8129,8 +7948,7 @@ func (fn *formulaFuncs) GAMMALN(argsList *list.List) formulaArg { // GAMMALNdotPRECISE function returns the natural logarithm of the Gamma // Function, Γ(n). The syntax of the function is: // -// GAMMALN.PRECISE(x) -// +// GAMMALN.PRECISE(x) func (fn *formulaFuncs) GAMMALNdotPRECISE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN.PRECISE requires 1 numeric argument") @@ -8149,8 +7967,7 @@ func (fn *formulaFuncs) GAMMALNdotPRECISE(argsList *list.List) formulaArg { // population will fall between the mean and a specified number of standard // deviations from the mean. The syntax of the function is: // -// GAUSS(z) -// +// GAUSS(z) func (fn *formulaFuncs) GAUSS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "GAUSS requires 1 numeric argument") @@ -8170,8 +7987,7 @@ func (fn *formulaFuncs) GAUSS(argsList *list.List) formulaArg { // GEOMEAN function calculates the geometric mean of a supplied set of values. // The syntax of the function is: // -// GEOMEAN(number1,[number2],...) -// +// GEOMEAN(number1,[number2],...) func (fn *formulaFuncs) GEOMEAN(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "GEOMEAN requires at least 1 numeric argument") @@ -8874,8 +8690,7 @@ func (fn *formulaFuncs) trendGrowth(name string, argsList *list.List) formulaArg // then extends the curve to calculate additional y-values for a further // supplied set of new x-values. The syntax of the function is: // -// GROWTH(known_y's,[known_x's],[new_x's],[const]) -// +// GROWTH(known_y's,[known_x's],[new_x's],[const]) func (fn *formulaFuncs) GROWTH(argsList *list.List) formulaArg { return fn.trendGrowth("GROWTH", argsList) } @@ -8883,8 +8698,7 @@ func (fn *formulaFuncs) GROWTH(argsList *list.List) formulaArg { // HARMEAN function calculates the harmonic mean of a supplied set of values. // The syntax of the function is: // -// HARMEAN(number1,[number2],...) -// +// HARMEAN(number1,[number2],...) func (fn *formulaFuncs) HARMEAN(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "HARMEAN requires at least 1 argument") @@ -8965,8 +8779,7 @@ func (fn *formulaFuncs) prepareHYPGEOMDISTArgs(name string, argsList *list.List) // can calculate the cumulative distribution or the probability density // function. The syntax of the function is: // -// HYPGEOM.DIST(sample_s,number_sample,population_s,number_pop,cumulative) -// +// HYPGEOM.DIST(sample_s,number_sample,population_s,number_pop,cumulative) func (fn *formulaFuncs) HYPGEOMdotDIST(argsList *list.List) formulaArg { args := fn.prepareHYPGEOMDISTArgs("HYPGEOM.DIST", argsList) if args.Type != ArgList { @@ -8991,8 +8804,7 @@ func (fn *formulaFuncs) HYPGEOMdotDIST(argsList *list.List) formulaArg { // for a given number of successes from a sample of a population. The syntax // of the function is: // -// HYPGEOMDIST(sample_s,number_sample,population_s,number_pop) -// +// HYPGEOMDIST(sample_s,number_sample,population_s,number_pop) func (fn *formulaFuncs) HYPGEOMDIST(argsList *list.List) formulaArg { args := fn.prepareHYPGEOMDISTArgs("HYPGEOMDIST", argsList) if args.Type != ArgList { @@ -9007,8 +8819,7 @@ func (fn *formulaFuncs) HYPGEOMDIST(argsList *list.List) formulaArg { // KURT function calculates the kurtosis of a supplied set of values. The // syntax of the function is: // -// KURT(number1,[number2],...) -// +// KURT(number1,[number2],...) func (fn *formulaFuncs) KURT(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "KURT requires at least 1 argument") @@ -9051,8 +8862,7 @@ func (fn *formulaFuncs) KURT(argsList *list.List) formulaArg { // function or the cumulative distribution function is used. The syntax of the // Expondist function is: // -// EXPON.DIST(x,lambda,cumulative) -// +// EXPON.DIST(x,lambda,cumulative) func (fn *formulaFuncs) EXPONdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "EXPON.DIST requires 3 arguments") @@ -9065,8 +8875,7 @@ func (fn *formulaFuncs) EXPONdotDIST(argsList *list.List) formulaArg { // function or the cumulative distribution function is used. The syntax of the // Expondist function is: // -// EXPONDIST(x,lambda,cumulative) -// +// EXPONDIST(x,lambda,cumulative) func (fn *formulaFuncs) EXPONDIST(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "EXPONDIST requires 3 arguments") @@ -9098,8 +8907,7 @@ func (fn *formulaFuncs) EXPONDIST(argsList *list.List) formulaArg { // frequently used to measure the degree of diversity between two data // sets. The syntax of the function is: // -// F.DIST(x,deg_freedom1,deg_freedom2,cumulative) -// +// F.DIST(x,deg_freedom1,deg_freedom2,cumulative) func (fn *formulaFuncs) FdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "F.DIST requires 4 arguments") @@ -9137,8 +8945,7 @@ func (fn *formulaFuncs) FdotDIST(argsList *list.List) formulaArg { // which measures the degree of diversity between two data sets. The syntax // of the function is: // -// FDIST(x,deg_freedom1,deg_freedom2) -// +// FDIST(x,deg_freedom1,deg_freedom2) func (fn *formulaFuncs) FDIST(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "FDIST requires 3 arguments") @@ -9176,8 +8983,7 @@ func (fn *formulaFuncs) FDIST(argsList *list.List) formulaArg { // Distribution, which measures the degree of diversity between two data sets. // The syntax of the function is: // -// F.DIST.RT(x,deg_freedom1,deg_freedom2) -// +// F.DIST.RT(x,deg_freedom1,deg_freedom2) func (fn *formulaFuncs) FdotDISTdotRT(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "F.DIST.RT requires 3 arguments") @@ -9216,8 +9022,7 @@ func (fn *formulaFuncs) prepareFinvArgs(name string, argsList *list.List) formul // FdotINV function calculates the inverse of the Cumulative F Distribution // for a supplied probability. The syntax of the F.Inv function is: // -// F.INV(probability,deg_freedom1,deg_freedom2) -// +// F.INV(probability,deg_freedom1,deg_freedom2) func (fn *formulaFuncs) FdotINV(argsList *list.List) formulaArg { args := fn.prepareFinvArgs("F.INV", argsList) if args.Type != ArgList { @@ -9231,8 +9036,7 @@ func (fn *formulaFuncs) FdotINV(argsList *list.List) formulaArg { // Probability Distribution for a supplied probability. The syntax of the // function is: // -// F.INV.RT(probability,deg_freedom1,deg_freedom2) -// +// F.INV.RT(probability,deg_freedom1,deg_freedom2) func (fn *formulaFuncs) FdotINVdotRT(argsList *list.List) formulaArg { args := fn.prepareFinvArgs("F.INV.RT", argsList) if args.Type != ArgList { @@ -9245,8 +9049,7 @@ func (fn *formulaFuncs) FdotINVdotRT(argsList *list.List) formulaArg { // FINV function calculates the inverse of the (right-tailed) F Probability // Distribution for a supplied probability. The syntax of the function is: // -// FINV(probability,deg_freedom1,deg_freedom2) -// +// FINV(probability,deg_freedom1,deg_freedom2) func (fn *formulaFuncs) FINV(argsList *list.List) formulaArg { args := fn.prepareFinvArgs("FINV", argsList) if args.Type != ArgList { @@ -9261,8 +9064,7 @@ func (fn *formulaFuncs) FINV(argsList *list.List) formulaArg { // supplied arrays are not significantly different. The syntax of the Ftest // function is: // -// F.TEST(array1,array2) -// +// F.TEST(array1,array2) func (fn *formulaFuncs) FdotTEST(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "F.TEST requires 2 arguments") @@ -9318,8 +9120,7 @@ func (fn *formulaFuncs) FdotTEST(argsList *list.List) formulaArg { // arrays are not significantly different. The syntax of the Ftest function // is: // -// FTEST(array1,array2) -// +// FTEST(array1,array2) func (fn *formulaFuncs) FTEST(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "FTEST requires 2 arguments") @@ -9331,8 +9132,7 @@ func (fn *formulaFuncs) FTEST(argsList *list.List) formulaArg { // Distribution Function of x, for a supplied probability. The syntax of the // function is: // -// LOGINV(probability,mean,standard_dev) -// +// LOGINV(probability,mean,standard_dev) func (fn *formulaFuncs) LOGINV(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "LOGINV requires 3 arguments") @@ -9365,8 +9165,7 @@ func (fn *formulaFuncs) LOGINV(argsList *list.List) formulaArg { // Distribution Function of x, for a supplied probability. The syntax of the // function is: // -// LOGNORM.INV(probability,mean,standard_dev) -// +// LOGNORM.INV(probability,mean,standard_dev) func (fn *formulaFuncs) LOGNORMdotINV(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "LOGNORM.INV requires 3 arguments") @@ -9378,8 +9177,7 @@ func (fn *formulaFuncs) LOGNORMdotINV(argsList *list.List) formulaArg { // Function or the Cumulative Log-Normal Distribution Function for a supplied // value of x. The syntax of the function is: // -// LOGNORM.DIST(x,mean,standard_dev,cumulative) -// +// LOGNORM.DIST(x,mean,standard_dev,cumulative) func (fn *formulaFuncs) LOGNORMdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "LOGNORM.DIST requires 4 arguments") @@ -9415,8 +9213,7 @@ func (fn *formulaFuncs) LOGNORMdotDIST(argsList *list.List) formulaArg { // LOGNORMDIST function calculates the Cumulative Log-Normal Distribution // Function at a supplied value of x. The syntax of the function is: // -// LOGNORMDIST(x,mean,standard_dev) -// +// LOGNORMDIST(x,mean,standard_dev) func (fn *formulaFuncs) LOGNORMDIST(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "LOGNORMDIST requires 3 arguments") @@ -9444,8 +9241,7 @@ func (fn *formulaFuncs) LOGNORMDIST(argsList *list.List) formulaArg { // frequently occurring values in the supplied data, the function returns the // lowest of these values The syntax of the function is: // -// MODE(number1,[number2],...) -// +// MODE(number1,[number2],...) func (fn *formulaFuncs) MODE(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "MODE requires at least 1 argument") @@ -9488,8 +9284,7 @@ func (fn *formulaFuncs) MODE(argsList *list.List) formulaArg { // (the most frequently occurring values) within a list of supplied numbers. // The syntax of the function is: // -// MODE.MULT(number1,[number2],...) -// +// MODE.MULT(number1,[number2],...) func (fn *formulaFuncs) MODEdotMULT(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "MODE.MULT requires at least 1 argument") @@ -9536,8 +9331,7 @@ func (fn *formulaFuncs) MODEdotMULT(argsList *list.List) formulaArg { // most frequently occurring values in the supplied data, the function returns // the lowest of these values. The syntax of the function is: // -// MODE.SNGL(number1,[number2],...) -// +// MODE.SNGL(number1,[number2],...) func (fn *formulaFuncs) MODEdotSNGL(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "MODE.SNGL requires at least 1 argument") @@ -9551,8 +9345,7 @@ func (fn *formulaFuncs) MODEdotSNGL(argsList *list.List) formulaArg { // before a required number of successes is achieved. The syntax of the // function is: // -// NEGBINOM.DIST(number_f,number_s,probability_s,cumulative) -// +// NEGBINOM.DIST(number_f,number_s,probability_s,cumulative) func (fn *formulaFuncs) NEGBINOMdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "NEGBINOM.DIST requires 4 arguments") @@ -9584,8 +9377,7 @@ func (fn *formulaFuncs) NEGBINOMdotDIST(argsList *list.List) formulaArg { // specified number of failures before a required number of successes is // achieved. The syntax of the function is: // -// NEGBINOMDIST(number_f,number_s,probability_s) -// +// NEGBINOMDIST(number_f,number_s,probability_s) func (fn *formulaFuncs) NEGBINOMDIST(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "NEGBINOMDIST requires 3 arguments") @@ -9610,8 +9402,7 @@ func (fn *formulaFuncs) NEGBINOMDIST(argsList *list.List) formulaArg { // the Cumulative Normal Distribution. Function for a supplied set of // parameters. The syntax of the function is: // -// NORM.DIST(x,mean,standard_dev,cumulative) -// +// NORM.DIST(x,mean,standard_dev,cumulative) func (fn *formulaFuncs) NORMdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "NORM.DIST requires 4 arguments") @@ -9623,8 +9414,7 @@ func (fn *formulaFuncs) NORMdotDIST(argsList *list.List) formulaArg { // Cumulative Normal Distribution. Function for a supplied set of parameters. // The syntax of the function is: // -// NORMDIST(x,mean,standard_dev,cumulative) -// +// NORMDIST(x,mean,standard_dev,cumulative) func (fn *formulaFuncs) NORMDIST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "NORMDIST requires 4 arguments") @@ -9655,8 +9445,7 @@ func (fn *formulaFuncs) NORMDIST(argsList *list.List) formulaArg { // Distribution Function for a supplied value of x, and a supplied // distribution mean & standard deviation. The syntax of the function is: // -// NORM.INV(probability,mean,standard_dev) -// +// NORM.INV(probability,mean,standard_dev) func (fn *formulaFuncs) NORMdotINV(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "NORM.INV requires 3 arguments") @@ -9668,8 +9457,7 @@ func (fn *formulaFuncs) NORMdotINV(argsList *list.List) formulaArg { // Distribution Function for a supplied value of x, and a supplied // distribution mean & standard deviation. The syntax of the function is: // -// NORMINV(probability,mean,standard_dev) -// +// NORMINV(probability,mean,standard_dev) func (fn *formulaFuncs) NORMINV(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "NORMINV requires 3 arguments") @@ -9701,8 +9489,7 @@ func (fn *formulaFuncs) NORMINV(argsList *list.List) formulaArg { // Distribution Function for a supplied value. The syntax of the function // is: // -// NORM.S.DIST(z) -// +// NORM.S.DIST(z) func (fn *formulaFuncs) NORMdotSdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "NORM.S.DIST requires 2 numeric arguments") @@ -9718,8 +9505,7 @@ func (fn *formulaFuncs) NORMdotSdotDIST(argsList *list.List) formulaArg { // NORMSDIST function calculates the Standard Normal Cumulative Distribution // Function for a supplied value. The syntax of the function is: // -// NORMSDIST(z) -// +// NORMSDIST(z) func (fn *formulaFuncs) NORMSDIST(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "NORMSDIST requires 1 numeric argument") @@ -9736,8 +9522,7 @@ func (fn *formulaFuncs) NORMSDIST(argsList *list.List) formulaArg { // Distribution Function for a supplied probability value. The syntax of the // function is: // -// NORMSINV(probability) -// +// NORMSINV(probability) func (fn *formulaFuncs) NORMSINV(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "NORMSINV requires 1 numeric argument") @@ -9753,8 +9538,7 @@ func (fn *formulaFuncs) NORMSINV(argsList *list.List) formulaArg { // Cumulative Distribution Function for a supplied probability value. The // syntax of the function is: // -// NORM.S.INV(probability) -// +// NORM.S.INV(probability) func (fn *formulaFuncs) NORMdotSdotINV(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "NORM.S.INV requires 1 numeric argument") @@ -9844,8 +9628,7 @@ func (fn *formulaFuncs) kth(name string, argsList *list.List) formulaArg { // LARGE function returns the k'th largest value from an array of numeric // values. The syntax of the function is: // -// LARGE(array,k) -// +// LARGE(array,k) func (fn *formulaFuncs) LARGE(argsList *list.List) formulaArg { return fn.kth("LARGE", argsList) } @@ -9853,8 +9636,7 @@ func (fn *formulaFuncs) LARGE(argsList *list.List) formulaArg { // MAX function returns the largest value from a supplied set of numeric // values. The syntax of the function is: // -// MAX(number1,[number2],...) -// +// MAX(number1,[number2],...) func (fn *formulaFuncs) MAX(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "MAX requires at least 1 argument") @@ -9867,8 +9649,7 @@ func (fn *formulaFuncs) MAX(argsList *list.List) formulaArg { // counting the logical value TRUE as the value 1. The syntax of the function // is: // -// MAXA(number1,[number2],...) -// +// MAXA(number1,[number2],...) func (fn *formulaFuncs) MAXA(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "MAXA requires at least 1 argument") @@ -9880,8 +9661,7 @@ func (fn *formulaFuncs) MAXA(argsList *list.List) formulaArg { // specified according to one or more criteria. The syntax of the function // is: // -// MAXIFS(max_range,criteria_range1,criteria1,[criteria_range2,criteria2],...) -// +// MAXIFS(max_range,criteria_range1,criteria1,[criteria_range2,criteria2],...) func (fn *formulaFuncs) MAXIFS(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "MAXIFS requires at least 3 arguments") @@ -9971,8 +9751,7 @@ func (fn *formulaFuncs) max(maxa bool, argsList *list.List) formulaArg { // MEDIAN function returns the statistical median (the middle value) of a list // of supplied numbers. The syntax of the function is: // -// MEDIAN(number1,[number2],...) -// +// MEDIAN(number1,[number2],...) func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "MEDIAN requires at least 1 argument") @@ -10017,8 +9796,7 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg { // MIN function returns the smallest value from a supplied set of numeric // values. The syntax of the function is: // -// MIN(number1,[number2],...) -// +// MIN(number1,[number2],...) func (fn *formulaFuncs) MIN(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "MIN requires at least 1 argument") @@ -10031,8 +9809,7 @@ func (fn *formulaFuncs) MIN(argsList *list.List) formulaArg { // counting the logical value TRUE as the value 1. The syntax of the function // is: // -// MINA(number1,[number2],...) -// +// MINA(number1,[number2],...) func (fn *formulaFuncs) MINA(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "MINA requires at least 1 argument") @@ -10044,8 +9821,7 @@ func (fn *formulaFuncs) MINA(argsList *list.List) formulaArg { // specified according to one or more criteria. The syntax of the function // is: // -// MINIFS(min_range,criteria_range1,criteria1,[criteria_range2,criteria2],...) -// +// MINIFS(min_range,criteria_range1,criteria1,[criteria_range2,criteria2],...) func (fn *formulaFuncs) MINIFS(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "MINIFS requires at least 3 arguments") @@ -10185,8 +9961,7 @@ func (fn *formulaFuncs) pearsonProduct(name string, argsList *list.List) formula // PEARSON function calculates the Pearson Product-Moment Correlation // Coefficient for two sets of values. The syntax of the function is: // -// PEARSON(array1,array2) -// +// PEARSON(array1,array2) func (fn *formulaFuncs) PEARSON(argsList *list.List) formulaArg { return fn.pearsonProduct("PEARSON", argsList) } @@ -10195,8 +9970,7 @@ func (fn *formulaFuncs) PEARSON(argsList *list.List) formulaArg { // which k% of the data values fall) for a supplied range of values and a // supplied k (between 0 & 1 exclusive).The syntax of the function is: // -// PERCENTILE.EXC(array,k) -// +// PERCENTILE.EXC(array,k) func (fn *formulaFuncs) PERCENTILEdotEXC(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "PERCENTILE.EXC requires 2 arguments") @@ -10232,8 +10006,7 @@ func (fn *formulaFuncs) PERCENTILEdotEXC(argsList *list.List) formulaArg { // which k% of the data values fall) for a supplied range of values and a // supplied k. The syntax of the function is: // -// PERCENTILE.INC(array,k) -// +// PERCENTILE.INC(array,k) func (fn *formulaFuncs) PERCENTILEdotINC(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "PERCENTILE.INC requires 2 arguments") @@ -10245,8 +10018,7 @@ func (fn *formulaFuncs) PERCENTILEdotINC(argsList *list.List) formulaArg { // k% of the data values fall) for a supplied range of values and a supplied // k. The syntax of the function is: // -// PERCENTILE(array,k) -// +// PERCENTILE(array,k) func (fn *formulaFuncs) PERCENTILE(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "PERCENTILE requires 2 arguments") @@ -10338,8 +10110,7 @@ func (fn *formulaFuncs) percentrank(name string, argsList *list.List) formulaArg // 1 (exclusive), of a specified value within a supplied array. The syntax of // the function is: // -// PERCENTRANK.EXC(array,x,[significance]) -// +// PERCENTRANK.EXC(array,x,[significance]) func (fn *formulaFuncs) PERCENTRANKdotEXC(argsList *list.List) formulaArg { return fn.percentrank("PERCENTRANK.EXC", argsList) } @@ -10348,8 +10119,7 @@ func (fn *formulaFuncs) PERCENTRANKdotEXC(argsList *list.List) formulaArg { // 1 (inclusive), of a specified value within a supplied array.The syntax of // the function is: // -// PERCENTRANK.INC(array,x,[significance]) -// +// PERCENTRANK.INC(array,x,[significance]) func (fn *formulaFuncs) PERCENTRANKdotINC(argsList *list.List) formulaArg { return fn.percentrank("PERCENTRANK.INC", argsList) } @@ -10357,8 +10127,7 @@ func (fn *formulaFuncs) PERCENTRANKdotINC(argsList *list.List) formulaArg { // PERCENTRANK function calculates the relative position of a specified value, // within a set of values, as a percentage. The syntax of the function is: // -// PERCENTRANK(array,x,[significance]) -// +// PERCENTRANK(array,x,[significance]) func (fn *formulaFuncs) PERCENTRANK(argsList *list.List) formulaArg { return fn.percentrank("PERCENTRANK", argsList) } @@ -10366,8 +10135,7 @@ func (fn *formulaFuncs) PERCENTRANK(argsList *list.List) formulaArg { // PERMUT function calculates the number of permutations of a specified number // of objects from a set of objects. The syntax of the function is: // -// PERMUT(number,number_chosen) -// +// PERMUT(number,number_chosen) func (fn *formulaFuncs) PERMUT(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "PERMUT requires 2 numeric arguments") @@ -10390,8 +10158,7 @@ func (fn *formulaFuncs) PERMUT(argsList *list.List) formulaArg { // repetitions, of a specified number of objects from a set. The syntax of // the function is: // -// PERMUTATIONA(number,number_chosen) -// +// PERMUTATIONA(number,number_chosen) func (fn *formulaFuncs) PERMUTATIONA(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "PERMUTATIONA requires 2 numeric arguments") @@ -10414,8 +10181,7 @@ func (fn *formulaFuncs) PERMUTATIONA(argsList *list.List) formulaArg { // PHI function returns the value of the density function for a standard normal // distribution for a supplied number. The syntax of the function is: // -// PHI(x) -// +// PHI(x) func (fn *formulaFuncs) PHI(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "PHI requires 1 argument") @@ -10430,8 +10196,7 @@ func (fn *formulaFuncs) PHI(argsList *list.List) formulaArg { // QUARTILE function returns a requested quartile of a supplied range of // values. The syntax of the function is: // -// QUARTILE(array,quart) -// +// QUARTILE(array,quart) func (fn *formulaFuncs) QUARTILE(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "QUARTILE requires 2 arguments") @@ -10453,8 +10218,7 @@ func (fn *formulaFuncs) QUARTILE(argsList *list.List) formulaArg { // values, based on a percentile range of 0 to 1 exclusive. The syntax of the // function is: // -// QUARTILE.EXC(array,quart) -// +// QUARTILE.EXC(array,quart) func (fn *formulaFuncs) QUARTILEdotEXC(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "QUARTILE.EXC requires 2 arguments") @@ -10475,8 +10239,7 @@ func (fn *formulaFuncs) QUARTILEdotEXC(argsList *list.List) formulaArg { // QUARTILEdotINC function returns a requested quartile of a supplied range of // values. The syntax of the function is: // -// QUARTILE.INC(array,quart) -// +// QUARTILE.INC(array,quart) func (fn *formulaFuncs) QUARTILEdotINC(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "QUARTILE.INC requires 2 arguments") @@ -10523,8 +10286,7 @@ func (fn *formulaFuncs) rank(name string, argsList *list.List) formulaArg { // supplied array of values. If there are duplicate values in the list, these // are given the same rank. The syntax of the function is: // -// RANK.EQ(number,ref,[order]) -// +// RANK.EQ(number,ref,[order]) func (fn *formulaFuncs) RANKdotEQ(argsList *list.List) formulaArg { return fn.rank("RANK.EQ", argsList) } @@ -10533,8 +10295,7 @@ func (fn *formulaFuncs) RANKdotEQ(argsList *list.List) formulaArg { // supplied array of values. If there are duplicate values in the list, these // are given the same rank. The syntax of the function is: // -// RANK(number,ref,[order]) -// +// RANK(number,ref,[order]) func (fn *formulaFuncs) RANK(argsList *list.List) formulaArg { return fn.rank("RANK", argsList) } @@ -10543,8 +10304,7 @@ func (fn *formulaFuncs) RANK(argsList *list.List) formulaArg { // Coefficient for two supplied sets of values. The syntax of the function // is: // -// RSQ(known_y's,known_x's) -// +// RSQ(known_y's,known_x's) func (fn *formulaFuncs) RSQ(argsList *list.List) formulaArg { return fn.pearsonProduct("RSQ", argsList) } @@ -10595,8 +10355,7 @@ func (fn *formulaFuncs) skew(name string, argsList *list.List) formulaArg { // SKEW function calculates the skewness of the distribution of a supplied set // of values. The syntax of the function is: // -// SKEW(number1,[number2],...) -// +// SKEW(number1,[number2],...) func (fn *formulaFuncs) SKEW(argsList *list.List) formulaArg { return fn.skew("SKEW", argsList) } @@ -10604,8 +10363,7 @@ func (fn *formulaFuncs) SKEW(argsList *list.List) formulaArg { // SKEWdotP function calculates the skewness of the distribution of a supplied // set of values. The syntax of the function is: // -// SKEW.P(number1,[number2],...) -// +// SKEW.P(number1,[number2],...) func (fn *formulaFuncs) SKEWdotP(argsList *list.List) formulaArg { return fn.skew("SKEW.P", argsList) } @@ -10615,8 +10373,7 @@ func (fn *formulaFuncs) SKEWdotP(argsList *list.List) formulaArg { // horizontal distance between any two points on the line, which is the rate // of change along the regression line. The syntax of the function is: // -// SLOPE(known_y's,known_x's) -// +// SLOPE(known_y's,known_x's) func (fn *formulaFuncs) SLOPE(argsList *list.List) formulaArg { return fn.pearsonProduct("SLOPE", argsList) } @@ -10624,8 +10381,7 @@ func (fn *formulaFuncs) SLOPE(argsList *list.List) formulaArg { // SMALL function returns the k'th smallest value from an array of numeric // values. The syntax of the function is: // -// SMALL(array,k) -// +// SMALL(array,k) func (fn *formulaFuncs) SMALL(argsList *list.List) formulaArg { return fn.kth("SMALL", argsList) } @@ -10634,8 +10390,7 @@ func (fn *formulaFuncs) SMALL(argsList *list.List) formulaArg { // characterized by a supplied mean and standard deviation. The syntax of the // function is: // -// STANDARDIZE(x,mean,standard_dev) -// +// STANDARDIZE(x,mean,standard_dev) func (fn *formulaFuncs) STANDARDIZE(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "STANDARDIZE requires 3 arguments") @@ -10678,8 +10433,7 @@ func (fn *formulaFuncs) stdevp(name string, argsList *list.List) formulaArg { // STDEVP function calculates the standard deviation of a supplied set of // values. The syntax of the function is: // -// STDEVP(number1,[number2],...) -// +// STDEVP(number1,[number2],...) func (fn *formulaFuncs) STDEVP(argsList *list.List) formulaArg { return fn.stdevp("STDEVP", argsList) } @@ -10687,8 +10441,7 @@ func (fn *formulaFuncs) STDEVP(argsList *list.List) formulaArg { // STDEVdotP function calculates the standard deviation of a supplied set of // values. // -// STDEV.P( number1, [number2], ... ) -// +// STDEV.P( number1, [number2], ... ) func (fn *formulaFuncs) STDEVdotP(argsList *list.List) formulaArg { return fn.stdevp("STDEV.P", argsList) } @@ -10696,8 +10449,7 @@ func (fn *formulaFuncs) STDEVdotP(argsList *list.List) formulaArg { // STDEVPA function calculates the standard deviation of a supplied set of // values. The syntax of the function is: // -// STDEVPA(number1,[number2],...) -// +// STDEVPA(number1,[number2],...) func (fn *formulaFuncs) STDEVPA(argsList *list.List) formulaArg { return fn.stdevp("STDEVPA", argsList) } @@ -10705,8 +10457,7 @@ func (fn *formulaFuncs) STDEVPA(argsList *list.List) formulaArg { // STEYX function calculates the standard error for the line of best fit, // through a supplied set of x- and y- values. The syntax of the function is: // -// STEYX(known_y's,known_x's) -// +// STEYX(known_y's,known_x's) func (fn *formulaFuncs) STEYX(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "STEYX requires 2 arguments") @@ -10766,8 +10517,7 @@ func getTDist(T, fDF, nType float64) float64 { // testing hypotheses on small sample data sets. The syntax of the function // is: // -// T.DIST(x,degrees_freedom,cumulative) -// +// T.DIST(x,degrees_freedom,cumulative) func (fn *formulaFuncs) TdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "T.DIST requires 3 arguments") @@ -10802,8 +10552,7 @@ func (fn *formulaFuncs) TdotDIST(argsList *list.List) formulaArg { // testing hypotheses on small sample data sets. The syntax of the function // is: // -// T.DIST.2T(x,degrees_freedom) -// +// T.DIST.2T(x,degrees_freedom) func (fn *formulaFuncs) TdotDISTdot2T(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "T.DIST.2T requires 2 arguments") @@ -10826,8 +10575,7 @@ func (fn *formulaFuncs) TdotDISTdot2T(argsList *list.List) formulaArg { // testing hypotheses on small sample data sets. The syntax of the function // is: // -// T.DIST.RT(x,degrees_freedom) -// +// T.DIST.RT(x,degrees_freedom) func (fn *formulaFuncs) TdotDISTdotRT(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "T.DIST.RT requires 2 arguments") @@ -10853,8 +10601,7 @@ func (fn *formulaFuncs) TdotDISTdotRT(argsList *list.List) formulaArg { // continuous probability distribution that is frequently used for testing // hypotheses on small sample data sets. The syntax of the function is: // -// TDIST(x,degrees_freedom,tails) -// +// TDIST(x,degrees_freedom,tails) func (fn *formulaFuncs) TDIST(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "TDIST requires 3 arguments") @@ -10880,8 +10627,7 @@ func (fn *formulaFuncs) TDIST(argsList *list.List) formulaArg { // frequently used for testing hypotheses on small sample data sets. The // syntax of the function is: // -// T.INV(probability,degrees_freedom) -// +// T.INV(probability,degrees_freedom) func (fn *formulaFuncs) TdotINV(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "T.INV requires 2 arguments") @@ -10917,8 +10663,7 @@ func (fn *formulaFuncs) TdotINV(argsList *list.List) formulaArg { // frequently used for testing hypotheses on small sample data sets. The // syntax of the function is: // -// T.INV.2T(probability,degrees_freedom) -// +// T.INV.2T(probability,degrees_freedom) func (fn *formulaFuncs) TdotINVdot2T(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "T.INV.2T requires 2 arguments") @@ -10946,8 +10691,7 @@ func (fn *formulaFuncs) TdotINVdot2T(argsList *list.List) formulaArg { // frequently used for testing hypotheses on small sample data sets. The // syntax of the function is: // -// TINV(probability,degrees_freedom) -// +// TINV(probability,degrees_freedom) func (fn *formulaFuncs) TINV(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "TINV requires 2 arguments") @@ -10960,8 +10704,7 @@ func (fn *formulaFuncs) TINV(argsList *list.List) formulaArg { // extends the linear trendline to calculate additional y-values for a further // supplied set of new x-values. The syntax of the function is: // -// TREND(known_y's,[known_x's],[new_x's],[const]) -// +// TREND(known_y's,[known_x's],[new_x's],[const]) func (fn *formulaFuncs) TREND(argsList *list.List) formulaArg { return fn.trendGrowth("TREND", argsList) } @@ -11055,8 +10798,7 @@ func (fn *formulaFuncs) tTest(mtx1, mtx2 [][]formulaArg, fTails, fTyp float64) f // likely to have come from the same two underlying populations with the same // mean. The syntax of the function is: // -// TTEST(array1,array2,tails,type) -// +// TTEST(array1,array2,tails,type) func (fn *formulaFuncs) TTEST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "TTEST requires 4 arguments") @@ -11087,8 +10829,7 @@ func (fn *formulaFuncs) TTEST(argsList *list.List) formulaArg { // likely to have come from the same two underlying populations with the same // mean. The syntax of the function is: // -// T.TEST(array1,array2,tails,type) -// +// T.TEST(array1,array2,tails,type) func (fn *formulaFuncs) TdotTEST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "T.TEST requires 4 arguments") @@ -11099,8 +10840,7 @@ func (fn *formulaFuncs) TdotTEST(argsList *list.List) formulaArg { // TRIMMEAN function calculates the trimmed mean (or truncated mean) of a // supplied set of values. The syntax of the function is: // -// TRIMMEAN(array,percent) -// +// TRIMMEAN(array,percent) func (fn *formulaFuncs) TRIMMEAN(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "TRIMMEAN requires 2 arguments") @@ -11185,8 +10925,7 @@ func (fn *formulaFuncs) vars(name string, argsList *list.List) formulaArg { // VAR function returns the sample variance of a supplied set of values. The // syntax of the function is: // -// VAR(number1,[number2],...) -// +// VAR(number1,[number2],...) func (fn *formulaFuncs) VAR(argsList *list.List) formulaArg { return fn.vars("VAR", argsList) } @@ -11194,8 +10933,7 @@ func (fn *formulaFuncs) VAR(argsList *list.List) formulaArg { // VARA function calculates the sample variance of a supplied set of values. // The syntax of the function is: // -// VARA(number1,[number2],...) -// +// VARA(number1,[number2],...) func (fn *formulaFuncs) VARA(argsList *list.List) formulaArg { return fn.vars("VARA", argsList) } @@ -11203,8 +10941,7 @@ func (fn *formulaFuncs) VARA(argsList *list.List) formulaArg { // VARP function returns the Variance of a given set of values. The syntax of // the function is: // -// VARP(number1,[number2],...) -// +// VARP(number1,[number2],...) func (fn *formulaFuncs) VARP(argsList *list.List) formulaArg { return fn.vars("VARP", argsList) } @@ -11212,8 +10949,7 @@ func (fn *formulaFuncs) VARP(argsList *list.List) formulaArg { // VARdotP function returns the Variance of a given set of values. The syntax // of the function is: // -// VAR.P(number1,[number2],...) -// +// VAR.P(number1,[number2],...) func (fn *formulaFuncs) VARdotP(argsList *list.List) formulaArg { return fn.vars("VAR.P", argsList) } @@ -11221,8 +10957,7 @@ func (fn *formulaFuncs) VARdotP(argsList *list.List) formulaArg { // VARdotS function calculates the sample variance of a supplied set of // values. The syntax of the function is: // -// VAR.S(number1,[number2],...) -// +// VAR.S(number1,[number2],...) func (fn *formulaFuncs) VARdotS(argsList *list.List) formulaArg { return fn.vars("VAR.S", argsList) } @@ -11230,8 +10965,7 @@ func (fn *formulaFuncs) VARdotS(argsList *list.List) formulaArg { // VARPA function returns the Variance of a given set of values. The syntax of // the function is: // -// VARPA(number1,[number2],...) -// +// VARPA(number1,[number2],...) func (fn *formulaFuncs) VARPA(argsList *list.List) formulaArg { return fn.vars("VARPA", argsList) } @@ -11240,8 +10974,7 @@ func (fn *formulaFuncs) VARPA(argsList *list.List) formulaArg { // Weibull Cumulative Distribution Function for a supplied set of parameters. // The syntax of the function is: // -// WEIBULL(x,alpha,beta,cumulative) -// +// WEIBULL(x,alpha,beta,cumulative) func (fn *formulaFuncs) WEIBULL(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "WEIBULL requires 4 arguments") @@ -11267,8 +11000,7 @@ func (fn *formulaFuncs) WEIBULL(argsList *list.List) formulaArg { // or the Weibull Cumulative Distribution Function for a supplied set of // parameters. The syntax of the function is: // -// WEIBULL.DIST(x,alpha,beta,cumulative) -// +// WEIBULL.DIST(x,alpha,beta,cumulative) func (fn *formulaFuncs) WEIBULLdotDIST(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "WEIBULL.DIST requires 4 arguments") @@ -11279,8 +11011,7 @@ func (fn *formulaFuncs) WEIBULLdotDIST(argsList *list.List) formulaArg { // ZdotTEST function calculates the one-tailed probability value of the // Z-Test. The syntax of the function is: // -// Z.TEST(array,x,[sigma]) -// +// Z.TEST(array,x,[sigma]) func (fn *formulaFuncs) ZdotTEST(argsList *list.List) formulaArg { argsLen := argsList.Len() if argsLen < 2 { @@ -11295,8 +11026,7 @@ func (fn *formulaFuncs) ZdotTEST(argsList *list.List) formulaArg { // ZTEST function calculates the one-tailed probability value of the Z-Test. // The syntax of the function is: // -// ZTEST(array,x,[sigma]) -// +// ZTEST(array,x,[sigma]) func (fn *formulaFuncs) ZTEST(argsList *list.List) formulaArg { argsLen := argsList.Len() if argsLen < 2 { @@ -11336,8 +11066,7 @@ func (fn *formulaFuncs) ZTEST(argsList *list.List) formulaArg { // ERRORdotTYPE function receives an error value and returns an integer, that // tells you the type of the supplied error. The syntax of the function is: // -// ERROR.TYPE(error_val) -// +// ERROR.TYPE(error_val) func (fn *formulaFuncs) ERRORdotTYPE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ERROR.TYPE requires 1 argument") @@ -11360,8 +11089,7 @@ func (fn *formulaFuncs) ERRORdotTYPE(argsList *list.List) formulaArg { // returns TRUE; Otherwise the function returns FALSE. The syntax of the // function is: // -// ISBLANK(value) -// +// ISBLANK(value) func (fn *formulaFuncs) ISBLANK(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISBLANK requires 1 argument") @@ -11384,8 +11112,7 @@ func (fn *formulaFuncs) ISBLANK(argsList *list.List) formulaArg { // logical value TRUE; If the supplied value is not an error or is the #N/A // error, the ISERR function returns FALSE. The syntax of the function is: // -// ISERR(value) -// +// ISERR(value) func (fn *formulaFuncs) ISERR(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISERR requires 1 argument") @@ -11410,8 +11137,7 @@ func (fn *formulaFuncs) ISERR(argsList *list.List) formulaArg { // an Excel Error, and if so, returns the logical value TRUE; Otherwise the // function returns FALSE. The syntax of the function is: // -// ISERROR(value) -// +// ISERROR(value) func (fn *formulaFuncs) ISERROR(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISERROR requires 1 argument") @@ -11436,8 +11162,7 @@ func (fn *formulaFuncs) ISERROR(argsList *list.List) formulaArg { // evaluates to an even number, and if so, returns TRUE; Otherwise, the // function returns FALSE. The syntax of the function is: // -// ISEVEN(value) -// +// ISEVEN(value) func (fn *formulaFuncs) ISEVEN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISEVEN requires 1 argument") @@ -11463,8 +11188,7 @@ func (fn *formulaFuncs) ISEVEN(argsList *list.List) formulaArg { // returns TRUE; Otherwise, the function returns FALSE. The syntax of the // function is: // -// ISFORMULA(reference) -// +// ISFORMULA(reference) func (fn *formulaFuncs) ISFORMULA(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISFORMULA requires 1 argument") @@ -11484,8 +11208,7 @@ func (fn *formulaFuncs) ISFORMULA(argsList *list.List) formulaArg { // logical value (i.e. evaluates to True or False). If so, the function // returns TRUE; Otherwise, it returns FALSE. The syntax of the function is: // -// ISLOGICAL(value) -// +// ISLOGICAL(value) func (fn *formulaFuncs) ISLOGICAL(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISLOGICAL requires 1 argument") @@ -11501,8 +11224,7 @@ func (fn *formulaFuncs) ISLOGICAL(argsList *list.List) formulaArg { // the Excel #N/A Error, and if so, returns TRUE; Otherwise the function // returns FALSE. The syntax of the function is: // -// ISNA(value) -// +// ISNA(value) func (fn *formulaFuncs) ISNA(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISNA requires 1 argument") @@ -11519,8 +11241,7 @@ func (fn *formulaFuncs) ISNA(argsList *list.List) formulaArg { // function returns TRUE; If the supplied value is text, the function returns // FALSE. The syntax of the function is: // -// ISNONTEXT(value) -// +// ISNONTEXT(value) func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISNONTEXT requires 1 argument") @@ -11537,8 +11258,7 @@ func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) formulaArg { // the function returns TRUE; Otherwise it returns FALSE. The syntax of the // function is: // -// ISNUMBER(value) -// +// ISNUMBER(value) func (fn *formulaFuncs) ISNUMBER(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISNUMBER requires 1 argument") @@ -11556,8 +11276,7 @@ func (fn *formulaFuncs) ISNUMBER(argsList *list.List) formulaArg { // to an odd number, and if so, returns TRUE; Otherwise, the function returns // FALSE. The syntax of the function is: // -// ISODD(value) -// +// ISODD(value) func (fn *formulaFuncs) ISODD(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISODD requires 1 argument") @@ -11583,8 +11302,7 @@ func (fn *formulaFuncs) ISODD(argsList *list.List) formulaArg { // function returns TRUE; Otherwise it returns FALSE. The syntax of the // function is: // -// ISREF(value) -// +// ISREF(value) func (fn *formulaFuncs) ISREF(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISREF requires 1 argument") @@ -11599,8 +11317,7 @@ func (fn *formulaFuncs) ISREF(argsList *list.List) formulaArg { // ISTEXT function tests if a supplied value is text, and if so, returns TRUE; // Otherwise, the function returns FALSE. The syntax of the function is: // -// ISTEXT(value) -// +// ISTEXT(value) func (fn *formulaFuncs) ISTEXT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISTEXT requires 1 argument") @@ -11615,8 +11332,7 @@ func (fn *formulaFuncs) ISTEXT(argsList *list.List) formulaArg { // N function converts data into a numeric value. The syntax of the function // is: // -// N(value) -// +// N(value) func (fn *formulaFuncs) N(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "N requires 1 argument") @@ -11638,8 +11354,7 @@ func (fn *formulaFuncs) N(argsList *list.List) formulaArg { // meaning 'value not available' and is produced when an Excel Formula is // unable to find a value that it needs. The syntax of the function is: // -// NA() -// +// NA() func (fn *formulaFuncs) NA(argsList *list.List) formulaArg { if argsList.Len() != 0 { return newErrorFormulaArg(formulaErrorVALUE, "NA accepts no arguments") @@ -11650,8 +11365,7 @@ func (fn *formulaFuncs) NA(argsList *list.List) formulaArg { // SHEET function returns the Sheet number for a specified reference. The // syntax of the function is: // -// SHEET([value]) -// +// SHEET([value]) func (fn *formulaFuncs) SHEET(argsList *list.List) formulaArg { if argsList.Len() > 1 { return newErrorFormulaArg(formulaErrorVALUE, "SHEET accepts at most 1 argument") @@ -11680,8 +11394,7 @@ func (fn *formulaFuncs) SHEET(argsList *list.List) formulaArg { // result includes sheets that are Visible, Hidden or Very Hidden. The syntax // of the function is: // -// SHEETS([reference]) -// +// SHEETS([reference]) func (fn *formulaFuncs) SHEETS(argsList *list.List) formulaArg { if argsList.Len() > 1 { return newErrorFormulaArg(formulaErrorVALUE, "SHEETS accepts at most 1 argument") @@ -11710,8 +11423,7 @@ func (fn *formulaFuncs) SHEETS(argsList *list.List) formulaArg { // TYPE function returns an integer that represents the value's data type. The // syntax of the function is: // -// TYPE(value) -// +// TYPE(value) func (fn *formulaFuncs) TYPE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "TYPE requires 1 argument") @@ -11737,8 +11449,7 @@ func (fn *formulaFuncs) TYPE(argsList *list.List) formulaArg { // supplied text; Otherwise, the function returns an empty text string. The // syntax of the function is: // -// T(value) -// +// T(value) func (fn *formulaFuncs) T(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "T requires 1 argument") @@ -11758,8 +11469,7 @@ func (fn *formulaFuncs) T(argsList *list.List) formulaArg { // AND function tests a number of supplied conditions and returns TRUE or // FALSE. The syntax of the function is: // -// AND(logical_test1,[logical_test2],...) -// +// AND(logical_test1,[logical_test2],...) func (fn *formulaFuncs) AND(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "AND requires at least 1 argument") @@ -11794,8 +11504,7 @@ func (fn *formulaFuncs) AND(argsList *list.List) formulaArg { // FALSE function returns the logical value FALSE. The syntax of the // function is: // -// FALSE() -// +// FALSE() func (fn *formulaFuncs) FALSE(argsList *list.List) formulaArg { if argsList.Len() != 0 { return newErrorFormulaArg(formulaErrorVALUE, "FALSE takes no arguments") @@ -11806,8 +11515,7 @@ func (fn *formulaFuncs) FALSE(argsList *list.List) formulaArg { // IFERROR function receives two values (or expressions) and tests if the // first of these evaluates to an error. The syntax of the function is: // -// IFERROR(value,value_if_error) -// +// IFERROR(value,value_if_error) func (fn *formulaFuncs) IFERROR(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "IFERROR requires 2 arguments") @@ -11827,8 +11535,7 @@ func (fn *formulaFuncs) IFERROR(argsList *list.List) formulaArg { // value; Otherwise the function returns the first supplied value. The syntax // of the function is: // -// IFNA(value,value_if_na) -// +// IFNA(value,value_if_na) func (fn *formulaFuncs) IFNA(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "IFNA requires 2 arguments") @@ -11845,8 +11552,7 @@ func (fn *formulaFuncs) IFNA(argsList *list.List) formulaArg { // the supplied conditions evaluate to TRUE, the function returns the #N/A // error. // -// IFS(logical_test1,value_if_true1,[logical_test2,value_if_true2],...) -// +// IFS(logical_test1,value_if_true1,[logical_test2,value_if_true2],...) func (fn *formulaFuncs) IFS(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "IFS requires at least 2 arguments") @@ -11863,8 +11569,7 @@ func (fn *formulaFuncs) IFS(argsList *list.List) formulaArg { // NOT function returns the opposite to a supplied logical value. The syntax // of the function is: // -// NOT(logical) -// +// NOT(logical) func (fn *formulaFuncs) NOT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "NOT requires 1 argument") @@ -11889,8 +11594,7 @@ func (fn *formulaFuncs) NOT(argsList *list.List) formulaArg { // OR function tests a number of supplied conditions and returns either TRUE // or FALSE. The syntax of the function is: // -// OR(logical_test1,[logical_test2],...) -// +// OR(logical_test1,[logical_test2],...) func (fn *formulaFuncs) OR(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "OR requires at least 1 argument") @@ -11929,9 +11633,7 @@ func (fn *formulaFuncs) OR(argsList *list.List) formulaArg { // returned if none of the supplied values match the test expression. The // syntax of the function is: // -// -// SWITCH(expression,value1,result1,[value2,result2],[value3,result3],...,[default]) -// +// SWITCH(expression,value1,result1,[value2,result2],[value3,result3],...,[default]) func (fn *formulaFuncs) SWITCH(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "SWITCH requires at least 3 arguments") @@ -11961,8 +11663,7 @@ func (fn *formulaFuncs) SWITCH(argsList *list.List) formulaArg { // TRUE function returns the logical value TRUE. The syntax of the function // is: // -// TRUE() -// +// TRUE() func (fn *formulaFuncs) TRUE(argsList *list.List) formulaArg { if argsList.Len() != 0 { return newErrorFormulaArg(formulaErrorVALUE, "TRUE takes no arguments") @@ -12006,8 +11707,7 @@ func calcXor(argsList *list.List) formulaArg { // of the supplied conditions evaluate to TRUE, and FALSE otherwise. The // syntax of the function is: // -// XOR(logical_test1,[logical_test2],...) -// +// XOR(logical_test1,[logical_test2],...) func (fn *formulaFuncs) XOR(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "XOR requires at least 1 argument") @@ -12020,8 +11720,7 @@ func (fn *formulaFuncs) XOR(argsList *list.List) formulaArg { // DATE returns a date, from a user-supplied year, month and day. The syntax // of the function is: // -// DATE(year,month,day) -// +// DATE(year,month,day) func (fn *formulaFuncs) DATE(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments") @@ -12072,8 +11771,7 @@ func calcDateDif(unit string, diff float64, seq []int, startArg, endArg formulaA // DATEDIF function calculates the number of days, months, or years between // two dates. The syntax of the function is: // -// DATEDIF(start_date,end_date,unit) -// +// DATEDIF(start_date,end_date,unit) func (fn *formulaFuncs) DATEDIF(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "DATEDIF requires 3 number arguments") @@ -12353,8 +12051,7 @@ func strToDate(str string) (int, int, int, bool, formulaArg) { // date, into the serial number that represents the date in Excels' date-time // code. The syntax of the function is: // -// DATEVALUE(date_text) -// +// DATEVALUE(date_text) func (fn *formulaFuncs) DATEVALUE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "DATEVALUE requires 1 argument") @@ -12376,8 +12073,7 @@ func (fn *formulaFuncs) DATEVALUE(argsList *list.List) formulaArg { // day is given as an integer ranging from 1 to 31. The syntax of the // function is: // -// DAY(serial_number) -// +// DAY(serial_number) func (fn *formulaFuncs) DAY(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "DAY requires exactly 1 argument") @@ -12409,8 +12105,7 @@ func (fn *formulaFuncs) DAY(argsList *list.List) formulaArg { // DAYS function returns the number of days between two supplied dates. The // syntax of the function is: // -// DAYS(end_date,start_date) -// +// DAYS(end_date,start_date) func (fn *formulaFuncs) DAYS(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "DAYS requires 2 arguments") @@ -12426,8 +12121,7 @@ func (fn *formulaFuncs) DAYS(argsList *list.List) formulaArg { // DAYS360 function returns the number of days between 2 dates, based on a // 360-day year (12 x 30 months). The syntax of the function is: // -// DAYS360(start_date,end_date,[method]) -// +// DAYS360(start_date,end_date,[method]) func (fn *formulaFuncs) DAYS360(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "DAYS360 requires at least 2 arguments") @@ -12477,8 +12171,7 @@ func (fn *formulaFuncs) DAYS360(argsList *list.List) formulaArg { // ISOWEEKNUM function returns the ISO week number of a supplied date. The // syntax of the function is: // -// ISOWEEKNUM(date) -// +// ISOWEEKNUM(date) func (fn *formulaFuncs) ISOWEEKNUM(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISOWEEKNUM requires 1 argument") @@ -12510,8 +12203,7 @@ func (fn *formulaFuncs) ISOWEEKNUM(argsList *list.List) formulaArg { // EDATE function returns a date that is a specified number of months before or // after a supplied start date. The syntax of function is: // -// EDATE(start_date,months) -// +// EDATE(start_date,months) func (fn *formulaFuncs) EDATE(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "EDATE requires 2 arguments") @@ -12565,8 +12257,7 @@ func (fn *formulaFuncs) EDATE(argsList *list.List) formulaArg { // number of months before or after an initial supplied start date. The syntax // of the function is: // -// EOMONTH(start_date,months) -// +// EOMONTH(start_date,months) func (fn *formulaFuncs) EOMONTH(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "EOMONTH requires 2 arguments") @@ -12613,8 +12304,7 @@ func (fn *formulaFuncs) EOMONTH(argsList *list.List) formulaArg { // HOUR function returns an integer representing the hour component of a // supplied Excel time. The syntax of the function is: // -// HOUR(serial_number) -// +// HOUR(serial_number) func (fn *formulaFuncs) HOUR(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "HOUR requires exactly 1 argument") @@ -12647,8 +12337,7 @@ func (fn *formulaFuncs) HOUR(argsList *list.List) formulaArg { // MINUTE function returns an integer representing the minute component of a // supplied Excel time. The syntax of the function is: // -// MINUTE(serial_number) -// +// MINUTE(serial_number) func (fn *formulaFuncs) MINUTE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "MINUTE requires exactly 1 argument") @@ -12679,8 +12368,7 @@ func (fn *formulaFuncs) MINUTE(argsList *list.List) formulaArg { // The month is given as an integer, ranging from 1 (January) to 12 // (December). The syntax of the function is: // -// MONTH(serial_number) -// +// MONTH(serial_number) func (fn *formulaFuncs) MONTH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "MONTH requires exactly 1 argument") @@ -12839,8 +12527,7 @@ func workdayIntl(endDate, sign int, holidays []int, weekendMask []byte, startDat // weekdays (Mon - Fri), excluding a supplied list of holidays. The syntax of // the function is: // -// NETWORKDAYS(start_date,end_date,[holidays]) -// +// NETWORKDAYS(start_date,end_date,[holidays]) func (fn *formulaFuncs) NETWORKDAYS(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS requires at least 2 arguments") @@ -12863,8 +12550,7 @@ func (fn *formulaFuncs) NETWORKDAYS(argsList *list.List) formulaArg { // the user to specify which days are counted as weekends and holidays. The // syntax of the function is: // -// NETWORKDAYS.INTL(start_date,end_date,[weekend],[holidays]) -// +// NETWORKDAYS.INTL(start_date,end_date,[weekend],[holidays]) func (fn *formulaFuncs) NETWORKDAYSdotINTL(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS.INTL requires at least 2 arguments") @@ -12922,8 +12608,7 @@ func (fn *formulaFuncs) NETWORKDAYSdotINTL(argsList *list.List) formulaArg { // (excluding weekends and holidays) ahead of a given start date. The syntax // of the function is: // -// WORKDAY(start_date,days,[holidays]) -// +// WORKDAY(start_date,days,[holidays]) func (fn *formulaFuncs) WORKDAY(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY requires at least 2 arguments") @@ -12946,8 +12631,7 @@ func (fn *formulaFuncs) WORKDAY(argsList *list.List) formulaArg { // function allows the user to specify which days of the week are counted as // weekends. The syntax of the function is: // -// WORKDAY.INTL(start_date,days,[weekend],[holidays]) -// +// WORKDAY.INTL(start_date,days,[weekend],[holidays]) func (fn *formulaFuncs) WORKDAYdotINTL(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY.INTL requires at least 2 arguments") @@ -13008,8 +12692,7 @@ func (fn *formulaFuncs) WORKDAYdotINTL(argsList *list.List) formulaArg { // YEAR function returns an integer representing the year of a supplied date. // The syntax of the function is: // -// YEAR(serial_number) -// +// YEAR(serial_number) func (fn *formulaFuncs) YEAR(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "YEAR requires exactly 1 argument") @@ -13156,8 +12839,7 @@ func getYearDays(year, basis int) int { // number of whole days between two supplied dates. The syntax of the // function is: // -// YEARFRAC(start_date,end_date,[basis]) -// +// YEARFRAC(start_date,end_date,[basis]) func (fn *formulaFuncs) YEARFRAC(argsList *list.List) formulaArg { if argsList.Len() != 2 && argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "YEARFRAC requires 3 or 4 arguments") @@ -13179,8 +12861,7 @@ func (fn *formulaFuncs) YEARFRAC(argsList *list.List) formulaArg { // NOW function returns the current date and time. The function receives no // arguments and therefore. The syntax of the function is: // -// NOW() -// +// NOW() func (fn *formulaFuncs) NOW(argsList *list.List) formulaArg { if argsList.Len() != 0 { return newErrorFormulaArg(formulaErrorVALUE, "NOW accepts no arguments") @@ -13193,8 +12874,7 @@ func (fn *formulaFuncs) NOW(argsList *list.List) formulaArg { // SECOND function returns an integer representing the second component of a // supplied Excel time. The syntax of the function is: // -// SECOND(serial_number) -// +// SECOND(serial_number) func (fn *formulaFuncs) SECOND(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SECOND requires exactly 1 argument") @@ -13226,8 +12906,7 @@ func (fn *formulaFuncs) SECOND(argsList *list.List) formulaArg { // decimal value that represents the time in Excel. The syntax of the // function is: // -// TIME(hour,minute,second) -// +// TIME(hour,minute,second) func (fn *formulaFuncs) TIME(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "TIME requires 3 number arguments") @@ -13248,8 +12927,7 @@ func (fn *formulaFuncs) TIME(argsList *list.List) formulaArg { // TIMEVALUE function converts a text representation of a time, into an Excel // time. The syntax of the function is: // -// TIMEVALUE(time_text) -// +// TIMEVALUE(time_text) func (fn *formulaFuncs) TIMEVALUE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "TIMEVALUE requires exactly 1 argument") @@ -13279,8 +12957,7 @@ func (fn *formulaFuncs) TIMEVALUE(argsList *list.List) formulaArg { // TODAY function returns the current date. The function has no arguments and // therefore. The syntax of the function is: // -// TODAY() -// +// TODAY() func (fn *formulaFuncs) TODAY(argsList *list.List) formulaArg { if argsList.Len() != 0 { return newErrorFormulaArg(formulaErrorVALUE, "TODAY accepts no arguments") @@ -13309,8 +12986,7 @@ func daysBetween(startDate, endDate int64) float64 { // WEEKDAY function returns an integer representing the day of the week for a // supplied date. The syntax of the function is: // -// WEEKDAY(serial_number,[return_type]) -// +// WEEKDAY(serial_number,[return_type]) func (fn *formulaFuncs) WEEKDAY(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "WEEKDAY requires at least 1 argument") @@ -13402,8 +13078,7 @@ func (fn *formulaFuncs) weeknum(snTime time.Time, returnType int) formulaArg { // WEEKNUM function returns an integer representing the week number (from 1 to // 53) of the year. The syntax of the function is: // -// WEEKNUM(serial_number,[return_type]) -// +// WEEKNUM(serial_number,[return_type]) func (fn *formulaFuncs) WEEKNUM(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "WEEKNUM requires at least 1 argument") @@ -13447,8 +13122,7 @@ func (fn *formulaFuncs) WEEKNUM(argsList *list.List) formulaArg { // CHAR function returns the character relating to a supplied character set // number (from 1 to 255). syntax of the function is: // -// CHAR(number) -// +// CHAR(number) func (fn *formulaFuncs) CHAR(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "CHAR requires 1 argument") @@ -13467,8 +13141,7 @@ func (fn *formulaFuncs) CHAR(argsList *list.List) formulaArg { // CLEAN removes all non-printable characters from a supplied text string. The // syntax of the function is: // -// CLEAN(text) -// +// CLEAN(text) func (fn *formulaFuncs) CLEAN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "CLEAN requires 1 argument") @@ -13486,8 +13159,7 @@ func (fn *formulaFuncs) CLEAN(argsList *list.List) formulaArg { // the associated numeric character set code used by your computer. The // syntax of the function is: // -// CODE(text) -// +// CODE(text) func (fn *formulaFuncs) CODE(argsList *list.List) formulaArg { return fn.code("CODE", argsList) } @@ -13510,8 +13182,7 @@ func (fn *formulaFuncs) code(name string, argsList *list.List) formulaArg { // CONCAT function joins together a series of supplied text strings into one // combined text string. // -// CONCAT(text1,[text2],...) -// +// CONCAT(text1,[text2],...) func (fn *formulaFuncs) CONCAT(argsList *list.List) formulaArg { return fn.concat("CONCAT", argsList) } @@ -13519,8 +13190,7 @@ func (fn *formulaFuncs) CONCAT(argsList *list.List) formulaArg { // CONCATENATE function joins together a series of supplied text strings into // one combined text string. // -// CONCATENATE(text1,[text2],...) -// +// CONCATENATE(text1,[text2],...) func (fn *formulaFuncs) CONCATENATE(argsList *list.List) formulaArg { return fn.concat("CONCATENATE", argsList) } @@ -13555,8 +13225,7 @@ func (fn *formulaFuncs) concat(name string, argsList *list.List) formulaArg { // equal and if so, returns TRUE; Otherwise, the function returns FALSE. The // function is case-sensitive. The syntax of the function is: // -// EXACT(text1,text2) -// +// EXACT(text1,text2) func (fn *formulaFuncs) EXACT(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "EXACT requires 2 arguments") @@ -13569,8 +13238,7 @@ func (fn *formulaFuncs) EXACT(argsList *list.List) formulaArg { // FIXED function rounds a supplied number to a specified number of decimal // places and then converts this into text. The syntax of the function is: // -// FIXED(number,[decimals],[no_commas]) -// +// FIXED(number,[decimals],[no_commas]) func (fn *formulaFuncs) FIXED(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "FIXED requires at least 1 argument") @@ -13619,8 +13287,7 @@ func (fn *formulaFuncs) FIXED(argsList *list.List) formulaArg { // within a supplied text string. The function is case-sensitive. The syntax // of the function is: // -// FIND(find_text,within_text,[start_num]) -// +// FIND(find_text,within_text,[start_num]) func (fn *formulaFuncs) FIND(argsList *list.List) formulaArg { return fn.find("FIND", argsList) } @@ -13630,8 +13297,7 @@ func (fn *formulaFuncs) FIND(argsList *list.List) formulaArg { // language. Otherwise, FINDB counts each character as 1. The syntax of the // function is: // -// FINDB(find_text,within_text,[start_num]) -// +// FINDB(find_text,within_text,[start_num]) func (fn *formulaFuncs) FINDB(argsList *list.List) formulaArg { return fn.find("FINDB", argsList) } @@ -13675,8 +13341,7 @@ func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg { // LEFT function returns a specified number of characters from the start of a // supplied text string. The syntax of the function is: // -// LEFT(text,[num_chars]) -// +// LEFT(text,[num_chars]) func (fn *formulaFuncs) LEFT(argsList *list.List) formulaArg { return fn.leftRight("LEFT", argsList) } @@ -13684,8 +13349,7 @@ func (fn *formulaFuncs) LEFT(argsList *list.List) formulaArg { // LEFTB returns the first character or characters in a text string, based on // the number of bytes you specify. The syntax of the function is: // -// LEFTB(text,[num_bytes]) -// +// LEFTB(text,[num_bytes]) func (fn *formulaFuncs) LEFTB(argsList *list.List) formulaArg { return fn.leftRight("LEFTB", argsList) } @@ -13723,8 +13387,7 @@ func (fn *formulaFuncs) leftRight(name string, argsList *list.List) formulaArg { // LEN returns the length of a supplied text string. The syntax of the // function is: // -// LEN(text) -// +// LEN(text) func (fn *formulaFuncs) LEN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LEN requires 1 string argument") @@ -13737,7 +13400,7 @@ func (fn *formulaFuncs) LEN(argsList *list.List) formulaArg { // as the default language. Otherwise LENB behaves the same as LEN, counting // 1 byte per character. The syntax of the function is: // -// LENB(text) +// LENB(text) // // TODO: the languages that support DBCS include Japanese, Chinese // (Simplified), Chinese (Traditional), and Korean. @@ -13751,8 +13414,7 @@ func (fn *formulaFuncs) LENB(argsList *list.List) formulaArg { // LOWER converts all characters in a supplied text string to lower case. The // syntax of the function is: // -// LOWER(text) -// +// LOWER(text) func (fn *formulaFuncs) LOWER(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LOWER requires 1 argument") @@ -13763,8 +13425,7 @@ func (fn *formulaFuncs) LOWER(argsList *list.List) formulaArg { // MID function returns a specified number of characters from the middle of a // supplied text string. The syntax of the function is: // -// MID(text,start_num,num_chars) -// +// MID(text,start_num,num_chars) func (fn *formulaFuncs) MID(argsList *list.List) formulaArg { return fn.mid("MID", argsList) } @@ -13773,8 +13434,7 @@ func (fn *formulaFuncs) MID(argsList *list.List) formulaArg { // at the position you specify, based on the number of bytes you specify. The // syntax of the function is: // -// MID(text,start_num,num_chars) -// +// MID(text,start_num,num_chars) func (fn *formulaFuncs) MIDB(argsList *list.List) formulaArg { return fn.mid("MIDB", argsList) } @@ -13815,8 +13475,7 @@ func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg { // upper case and all other characters are lower case). The syntax of the // function is: // -// PROPER(text) -// +// PROPER(text) func (fn *formulaFuncs) PROPER(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "PROPER requires 1 argument") @@ -13837,8 +13496,7 @@ func (fn *formulaFuncs) PROPER(argsList *list.List) formulaArg { // REPLACE function replaces all or part of a text string with another string. // The syntax of the function is: // -// REPLACE(old_text,start_num,num_chars,new_text) -// +// REPLACE(old_text,start_num,num_chars,new_text) func (fn *formulaFuncs) REPLACE(argsList *list.List) formulaArg { return fn.replace("REPLACE", argsList) } @@ -13846,8 +13504,7 @@ func (fn *formulaFuncs) REPLACE(argsList *list.List) formulaArg { // REPLACEB replaces part of a text string, based on the number of bytes you // specify, with a different text string. // -// REPLACEB(old_text,start_num,num_chars,new_text) -// +// REPLACEB(old_text,start_num,num_chars,new_text) func (fn *formulaFuncs) REPLACEB(argsList *list.List) formulaArg { return fn.replace("REPLACEB", argsList) } @@ -13885,8 +13542,7 @@ func (fn *formulaFuncs) replace(name string, argsList *list.List) formulaArg { // REPT function returns a supplied text string, repeated a specified number // of times. The syntax of the function is: // -// REPT(text,number_times) -// +// REPT(text,number_times) func (fn *formulaFuncs) REPT(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "REPT requires 2 arguments") @@ -13915,8 +13571,7 @@ func (fn *formulaFuncs) REPT(argsList *list.List) formulaArg { // RIGHT function returns a specified number of characters from the end of a // supplied text string. The syntax of the function is: // -// RIGHT(text,[num_chars]) -// +// RIGHT(text,[num_chars]) func (fn *formulaFuncs) RIGHT(argsList *list.List) formulaArg { return fn.leftRight("RIGHT", argsList) } @@ -13924,8 +13579,7 @@ func (fn *formulaFuncs) RIGHT(argsList *list.List) formulaArg { // RIGHTB returns the last character or characters in a text string, based on // the number of bytes you specify. The syntax of the function is: // -// RIGHTB(text,[num_bytes]) -// +// RIGHTB(text,[num_bytes]) func (fn *formulaFuncs) RIGHTB(argsList *list.List) formulaArg { return fn.leftRight("RIGHTB", argsList) } @@ -13933,8 +13587,7 @@ func (fn *formulaFuncs) RIGHTB(argsList *list.List) formulaArg { // SUBSTITUTE function replaces one or more instances of a given text string, // within an original text string. The syntax of the function is: // -// SUBSTITUTE(text,old_text,new_text,[instance_num]) -// +// SUBSTITUTE(text,old_text,new_text,[instance_num]) func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg { if argsList.Len() != 3 && argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "SUBSTITUTE requires 3 or 4 arguments") @@ -13980,8 +13633,7 @@ func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg { // combined text string. The user can specify a delimiter to add between the // individual text items, if required. The syntax of the function is: // -// TEXTJOIN([delimiter],[ignore_empty],text1,[text2],...) -// +// TEXTJOIN([delimiter],[ignore_empty],text1,[text2],...) func (fn *formulaFuncs) TEXTJOIN(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "TEXTJOIN requires at least 3 arguments") @@ -14038,8 +13690,7 @@ func textJoin(arg *list.Element, arr []string, ignoreEmpty bool) ([]string, form // words or characters) from a supplied text string. The syntax of the // function is: // -// TRIM(text) -// +// TRIM(text) func (fn *formulaFuncs) TRIM(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "TRIM requires 1 argument") @@ -14050,8 +13701,7 @@ func (fn *formulaFuncs) TRIM(argsList *list.List) formulaArg { // UNICHAR returns the Unicode character that is referenced by the given // numeric value. The syntax of the function is: // -// UNICHAR(number) -// +// UNICHAR(number) func (fn *formulaFuncs) UNICHAR(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "UNICHAR requires 1 argument") @@ -14069,8 +13719,7 @@ func (fn *formulaFuncs) UNICHAR(argsList *list.List) formulaArg { // UNICODE function returns the code point for the first character of a // supplied text string. The syntax of the function is: // -// UNICODE(text) -// +// UNICODE(text) func (fn *formulaFuncs) UNICODE(argsList *list.List) formulaArg { return fn.code("UNICODE", argsList) } @@ -14078,8 +13727,7 @@ func (fn *formulaFuncs) UNICODE(argsList *list.List) formulaArg { // UPPER converts all characters in a supplied text string to upper case. The // syntax of the function is: // -// UPPER(text) -// +// UPPER(text) func (fn *formulaFuncs) UPPER(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "UPPER requires 1 argument") @@ -14090,8 +13738,7 @@ func (fn *formulaFuncs) UPPER(argsList *list.List) formulaArg { // VALUE function converts a text string into a numeric value. The syntax of // the function is: // -// VALUE(text) -// +// VALUE(text) func (fn *formulaFuncs) VALUE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "VALUE requires 1 argument") @@ -14131,8 +13778,7 @@ func (fn *formulaFuncs) VALUE(argsList *list.List) formulaArg { // condition evaluates to TRUE, and another result if the condition evaluates // to FALSE. The syntax of the function is: // -// IF(logical_test,value_if_true,value_if_false) -// +// IF(logical_test,value_if_true,value_if_false) func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "IF requires at least 1 argument") @@ -14185,8 +13831,7 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { // ADDRESS function takes a row and a column number and returns a cell // reference as a text string. The syntax of the function is: // -// ADDRESS(row_num,column_num,[abs_num],[a1],[sheet_text]) -// +// ADDRESS(row_num,column_num,[abs_num],[a1],[sheet_text]) func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "ADDRESS requires at least 2 arguments") @@ -14240,8 +13885,7 @@ func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg { // CHOOSE function returns a value from an array, that corresponds to a // supplied index number (position). The syntax of the function is: // -// CHOOSE(index_num,value1,[value2],...) -// +// CHOOSE(index_num,value1,[value2],...) func (fn *formulaFuncs) CHOOSE(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "CHOOSE requires 2 arguments") @@ -14388,8 +14032,7 @@ func compareFormulaArgMatrix(lhs, rhs, matchMode formulaArg, caseSensitive bool) // COLUMN function returns the first column number within a supplied reference // or the number of the current column. The syntax of the function is: // -// COLUMN([reference]) -// +// COLUMN([reference]) func (fn *formulaFuncs) COLUMN(argsList *list.List) formulaArg { if argsList.Len() > 1 { return newErrorFormulaArg(formulaErrorVALUE, "COLUMN requires at most 1 argument") @@ -14450,8 +14093,7 @@ func calcColumnsMinMax(argsList *list.List) (min, max int) { // COLUMNS function receives an Excel range and returns the number of columns // that are contained within the range. The syntax of the function is: // -// COLUMNS(array) -// +// COLUMNS(array) func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COLUMNS requires 1 argument") @@ -14473,8 +14115,7 @@ func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { // FORMULATEXT function returns a formula as a text string. The syntax of the // function is: // -// FORMULATEXT(reference) -// +// FORMULATEXT(reference) func (fn *formulaFuncs) FORMULATEXT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "FORMULATEXT requires 1 argument") @@ -14540,8 +14181,7 @@ func checkHVLookupArgs(name string, argsList *list.List) (idx int, lookupValue, // (or table), and returns the corresponding value from another row of the // array. The syntax of the function is: // -// HLOOKUP(lookup_value,table_array,row_index_num,[range_lookup]) -// +// HLOOKUP(lookup_value,table_array,row_index_num,[range_lookup]) func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { rowIdx, lookupValue, tableArray, matchMode, errArg := checkHVLookupArgs("HLOOKUP", argsList) if errArg.Type == ArgError { @@ -14570,8 +14210,7 @@ func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { // HYPERLINK function creates a hyperlink to a specified location. The syntax // of the function is: // -// HYPERLINK(link_location,[friendly_name]) -// +// HYPERLINK(link_location,[friendly_name]) func (fn *formulaFuncs) HYPERLINK(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "HYPERLINK requires at least 1 argument") @@ -14630,8 +14269,7 @@ func calcMatch(matchType int, criteria *formulaCriteria, lookupArray []formulaAr // should return the position of the closest match (above or below), if an // exact match is not found. The syntax of the Match function is: // -// MATCH(lookup_value,lookup_array,[match_type]) -// +// MATCH(lookup_value,lookup_array,[match_type]) func (fn *formulaFuncs) MATCH(argsList *list.List) formulaArg { if argsList.Len() != 2 && argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "MATCH requires 1 or 2 arguments") @@ -14667,8 +14305,7 @@ func (fn *formulaFuncs) MATCH(argsList *list.List) formulaArg { // a horizontal range of cells into a vertical range and vice versa). The // syntax of the function is: // -// TRANSPOSE(array) -// +// TRANSPOSE(array) func (fn *formulaFuncs) TRANSPOSE(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "TRANSPOSE requires 1 argument") @@ -14736,8 +14373,7 @@ start: // data array (or table), and returns the corresponding value from another // column of the array. The syntax of the function is: // -// VLOOKUP(lookup_value,table_array,col_index_num,[range_lookup]) -// +// VLOOKUP(lookup_value,table_array,col_index_num,[range_lookup]) func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg { colIdx, lookupValue, tableArray, matchMode, errArg := checkHVLookupArgs("VLOOKUP", argsList) if errArg.Type == ArgError { @@ -14992,8 +14628,7 @@ func (fn *formulaFuncs) xlookup(lookupRows, lookupCols, returnArrayRows, returnA // XLOOKUP can return the closest (approximate) match. The syntax of the // function is: // -// XLOOKUP(lookup_value,lookup_array,return_array,[if_not_found],[match_mode],[search_mode]) -// +// XLOOKUP(lookup_value,lookup_array,return_array,[if_not_found],[match_mode],[search_mode]) func (fn *formulaFuncs) XLOOKUP(argsList *list.List) formulaArg { args := fn.prepareXlookupArgs(argsList) if args.Type != ArgList { @@ -15029,8 +14664,7 @@ func (fn *formulaFuncs) XLOOKUP(argsList *list.List) formulaArg { // INDEX function returns a reference to a cell that lies in a specified row // and column of a range of cells. The syntax of the function is: // -// INDEX(array,row_num,[col_num]) -// +// INDEX(array,row_num,[col_num]) func (fn *formulaFuncs) INDEX(argsList *list.List) formulaArg { if argsList.Len() < 2 || argsList.Len() > 3 { return newErrorFormulaArg(formulaErrorVALUE, "INDEX requires 2 or 3 arguments") @@ -15070,8 +14704,7 @@ func (fn *formulaFuncs) INDEX(argsList *list.List) formulaArg { // INDIRECT function converts a text string into a cell reference. The syntax // of the Indirect function is: // -// INDIRECT(ref_text,[a1]) -// +// INDIRECT(ref_text,[a1]) func (fn *formulaFuncs) INDIRECT(argsList *list.List) formulaArg { if argsList.Len() != 1 && argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "INDIRECT requires 1 or 2 arguments") @@ -15133,8 +14766,7 @@ func (fn *formulaFuncs) INDIRECT(argsList *list.List) formulaArg { // one-row range, and returns the corresponding value from another one-column // or one-row range. The syntax of the function is: // -// LOOKUP(lookup_value,lookup_vector,[result_vector]) -// +// LOOKUP(lookup_value,lookup_vector,[result_vector]) func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg { arrayForm, lookupValue, lookupVector, errArg := checkLookupArgs(argsList) if errArg.Type == ArgError { @@ -15177,8 +14809,7 @@ func lookupCol(arr formulaArg, idx int) []formulaArg { // ROW function returns the first row number within a supplied reference or // the number of the current row. The syntax of the function is: // -// ROW([reference]) -// +// ROW([reference]) func (fn *formulaFuncs) ROW(argsList *list.List) formulaArg { if argsList.Len() > 1 { return newErrorFormulaArg(formulaErrorVALUE, "ROW requires at most 1 argument") @@ -15239,8 +14870,7 @@ func calcRowsMinMax(argsList *list.List) (min, max int) { // ROWS function takes an Excel range and returns the number of rows that are // contained within the range. The syntax of the function is: // -// ROWS(array) -// +// ROWS(array) func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ROWS requires 1 argument") @@ -15265,8 +14895,7 @@ func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg { // non-alphanumeric characters with the percentage symbol (%) and a // hexadecimal number. The syntax of the function is: // -// ENCODEURL(url) -// +// ENCODEURL(url) func (fn *formulaFuncs) ENCODEURL(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ENCODEURL requires 1 argument") @@ -15285,8 +14914,7 @@ func validateFrequency(freq float64) bool { // ACCRINT function returns the accrued interest in a security that pays // periodic interest. The syntax of the function is: // -// ACCRINT(issue,first_interest,settlement,rate,par,frequency,[basis],[calc_method]) -// +// ACCRINT(issue,first_interest,settlement,rate,par,frequency,[basis],[calc_method]) func (fn *formulaFuncs) ACCRINT(argsList *list.List) formulaArg { if argsList.Len() < 6 { return newErrorFormulaArg(formulaErrorVALUE, "ACCRINT requires at least 6 arguments") @@ -15329,8 +14957,7 @@ func (fn *formulaFuncs) ACCRINT(argsList *list.List) formulaArg { // ACCRINTM function returns the accrued interest in a security that pays // interest at maturity. The syntax of the function is: // -// ACCRINTM(issue,settlement,rate,[par],[basis]) -// +// ACCRINTM(issue,settlement,rate,[par],[basis]) func (fn *formulaFuncs) ACCRINTM(argsList *list.List) formulaArg { if argsList.Len() != 4 && argsList.Len() != 5 { return newErrorFormulaArg(formulaErrorVALUE, "ACCRINTM requires 4 or 5 arguments") @@ -15417,8 +15044,7 @@ func (fn *formulaFuncs) prepareAmorArgs(name string, argsList *list.List) formul // The function calculates the prorated linear depreciation of an asset for a // specified accounting period. The syntax of the function is: // -// AMORDEGRC(cost,date_purchased,first_period,salvage,period,rate,[basis]) -// +// AMORDEGRC(cost,date_purchased,first_period,salvage,period,rate,[basis]) func (fn *formulaFuncs) AMORDEGRC(argsList *list.List) formulaArg { if argsList.Len() != 6 && argsList.Len() != 7 { return newErrorFormulaArg(formulaErrorVALUE, "AMORDEGRC requires 6 or 7 arguments") @@ -15468,8 +15094,7 @@ func (fn *formulaFuncs) AMORDEGRC(argsList *list.List) formulaArg { // The function calculates the prorated linear depreciation of an asset for a // specified accounting period. The syntax of the function is: // -// AMORLINC(cost,date_purchased,first_period,salvage,period,rate,[basis]) -// +// AMORLINC(cost,date_purchased,first_period,salvage,period,rate,[basis]) func (fn *formulaFuncs) AMORLINC(argsList *list.List) formulaArg { if argsList.Len() != 6 && argsList.Len() != 7 { return newErrorFormulaArg(formulaErrorVALUE, "AMORLINC requires 6 or 7 arguments") @@ -15598,8 +15223,7 @@ func coupdays(from, to time.Time, basis int) float64 { // COUPDAYBS function calculates the number of days from the beginning of a // coupon's period to the settlement date. The syntax of the function is: // -// COUPDAYBS(settlement,maturity,frequency,[basis]) -// +// COUPDAYBS(settlement,maturity,frequency,[basis]) func (fn *formulaFuncs) COUPDAYBS(argsList *list.List) formulaArg { args := fn.prepareCouponArgs("COUPDAYBS", argsList) if args.Type != ArgList { @@ -15613,8 +15237,7 @@ func (fn *formulaFuncs) COUPDAYBS(argsList *list.List) formulaArg { // COUPDAYS function calculates the number of days in a coupon period that // contains the settlement date. The syntax of the function is: // -// COUPDAYS(settlement,maturity,frequency,[basis]) -// +// COUPDAYS(settlement,maturity,frequency,[basis]) func (fn *formulaFuncs) COUPDAYS(argsList *list.List) formulaArg { args := fn.prepareCouponArgs("COUPDAYS", argsList) if args.Type != ArgList { @@ -15633,8 +15256,7 @@ func (fn *formulaFuncs) COUPDAYS(argsList *list.List) formulaArg { // COUPDAYSNC function calculates the number of days from the settlement date // to the next coupon date. The syntax of the function is: // -// COUPDAYSNC(settlement,maturity,frequency,[basis]) -// +// COUPDAYSNC(settlement,maturity,frequency,[basis]) func (fn *formulaFuncs) COUPDAYSNC(argsList *list.List) formulaArg { args := fn.prepareCouponArgs("COUPDAYSNC", argsList) if args.Type != ArgList { @@ -15684,8 +15306,7 @@ func (fn *formulaFuncs) coupons(name string, arg formulaArg) formulaArg { // security's settlement date and maturity date, rounded up to the nearest // whole coupon. The syntax of the function is: // -// COUPNCD(settlement,maturity,frequency,[basis]) -// +// COUPNCD(settlement,maturity,frequency,[basis]) func (fn *formulaFuncs) COUPNCD(argsList *list.List) formulaArg { args := fn.prepareCouponArgs("COUPNCD", argsList) if args.Type != ArgList { @@ -15698,8 +15319,7 @@ func (fn *formulaFuncs) COUPNCD(argsList *list.List) formulaArg { // security's settlement date and maturity date, rounded up to the nearest // whole coupon. The syntax of the function is: // -// COUPNUM(settlement,maturity,frequency,[basis]) -// +// COUPNUM(settlement,maturity,frequency,[basis]) func (fn *formulaFuncs) COUPNUM(argsList *list.List) formulaArg { args := fn.prepareCouponArgs("COUPNUM", argsList) if args.Type != ArgList { @@ -15712,8 +15332,7 @@ func (fn *formulaFuncs) COUPNUM(argsList *list.List) formulaArg { // COUPPCD function returns the previous coupon date, before the settlement // date for a security. The syntax of the function is: // -// COUPPCD(settlement,maturity,frequency,[basis]) -// +// COUPPCD(settlement,maturity,frequency,[basis]) func (fn *formulaFuncs) COUPPCD(argsList *list.List) formulaArg { args := fn.prepareCouponArgs("COUPPCD", argsList) if args.Type != ArgList { @@ -15725,8 +15344,7 @@ func (fn *formulaFuncs) COUPPCD(argsList *list.List) formulaArg { // CUMIPMT function calculates the cumulative interest paid on a loan or // investment, between two specified periods. The syntax of the function is: // -// CUMIPMT(rate,nper,pv,start_period,end_period,type) -// +// CUMIPMT(rate,nper,pv,start_period,end_period,type) func (fn *formulaFuncs) CUMIPMT(argsList *list.List) formulaArg { return fn.cumip("CUMIPMT", argsList) } @@ -15735,8 +15353,7 @@ func (fn *formulaFuncs) CUMIPMT(argsList *list.List) formulaArg { // loan or investment, between two specified periods. The syntax of the // function is: // -// CUMPRINC(rate,nper,pv,start_period,end_period,type) -// +// CUMPRINC(rate,nper,pv,start_period,end_period,type) func (fn *formulaFuncs) CUMPRINC(argsList *list.List) formulaArg { return fn.cumip("CUMPRINC", argsList) } @@ -15803,8 +15420,7 @@ func calcDbArgsCompare(cost, salvage, life, period formulaArg) bool { // Declining Balance Method, for each period of the asset's lifetime. The // syntax of the function is: // -// DB(cost,salvage,life,period,[month]) -// +// DB(cost,salvage,life,period,[month]) func (fn *formulaFuncs) DB(argsList *list.List) formulaArg { if argsList.Len() < 4 { return newErrorFormulaArg(formulaErrorVALUE, "DB requires at least 4 arguments") @@ -15860,8 +15476,7 @@ func (fn *formulaFuncs) DB(argsList *list.List) formulaArg { // Declining Balance Method, or another specified depreciation rate. The // syntax of the function is: // -// DDB(cost,salvage,life,period,[factor]) -// +// DDB(cost,salvage,life,period,[factor]) func (fn *formulaFuncs) DDB(argsList *list.List) formulaArg { if argsList.Len() < 4 { return newErrorFormulaArg(formulaErrorVALUE, "DDB requires at least 4 arguments") @@ -15945,8 +15560,7 @@ func (fn *formulaFuncs) prepareDataValueArgs(n int, argsList *list.List) formula // DISC function calculates the Discount Rate for a security. The syntax of // the function is: // -// DISC(settlement,maturity,pr,redemption,[basis]) -// +// DISC(settlement,maturity,pr,redemption,[basis]) func (fn *formulaFuncs) DISC(argsList *list.List) formulaArg { if argsList.Len() != 4 && argsList.Len() != 5 { return newErrorFormulaArg(formulaErrorVALUE, "DISC requires 4 or 5 arguments") @@ -15989,8 +15603,7 @@ func (fn *formulaFuncs) DISC(argsList *list.List) formulaArg { // DOLLARDE function converts a dollar value in fractional notation, into a // dollar value expressed as a decimal. The syntax of the function is: // -// DOLLARDE(fractional_dollar,fraction) -// +// DOLLARDE(fractional_dollar,fraction) func (fn *formulaFuncs) DOLLARDE(argsList *list.List) formulaArg { return fn.dollar("DOLLARDE", argsList) } @@ -15999,8 +15612,7 @@ func (fn *formulaFuncs) DOLLARDE(argsList *list.List) formulaArg { // dollar value that is expressed in fractional notation. The syntax of the // function is: // -// DOLLARFR(decimal_dollar,fraction) -// +// DOLLARFR(decimal_dollar,fraction) func (fn *formulaFuncs) DOLLARFR(argsList *list.List) formulaArg { return fn.dollar("DOLLARFR", argsList) } @@ -16115,8 +15727,7 @@ func (fn *formulaFuncs) duration(settlement, maturity, coupon, yld, frequency, b // Duration) of a security that pays periodic interest, assuming a par value // of $100. The syntax of the function is: // -// DURATION(settlement,maturity,coupon,yld,frequency,[basis]) -// +// DURATION(settlement,maturity,coupon,yld,frequency,[basis]) func (fn *formulaFuncs) DURATION(argsList *list.List) formulaArg { args := fn.prepareDurationArgs("DURATION", argsList) if args.Type != ArgList { @@ -16129,8 +15740,7 @@ func (fn *formulaFuncs) DURATION(argsList *list.List) formulaArg { // nominal interest rate and number of compounding periods per year. The // syntax of the function is: // -// EFFECT(nominal_rate,npery) -// +// EFFECT(nominal_rate,npery) func (fn *formulaFuncs) EFFECT(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "EFFECT requires 2 arguments") @@ -16154,8 +15764,7 @@ func (fn *formulaFuncs) EFFECT(argsList *list.List) formulaArg { // participating currency to another by using the euro as an intermediary // (triangulation). The syntax of the function is: // -// EUROCONVERT(number,sourcecurrency,targetcurrency[,fullprecision,triangulationprecision]) -// +// EUROCONVERT(number,sourcecurrency,targetcurrency[,fullprecision,triangulationprecision]) func (fn *formulaFuncs) EUROCONVERT(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "EUROCONVERT requires at least 3 arguments") @@ -16235,8 +15844,7 @@ func (fn *formulaFuncs) EUROCONVERT(argsList *list.List) formulaArg { // constant payments and a constant interest rate. The syntax of the function // is: // -// FV(rate,nper,[pmt],[pv],[type]) -// +// FV(rate,nper,[pmt],[pv],[type]) func (fn *formulaFuncs) FV(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "FV requires at least 3 arguments") @@ -16279,8 +15887,7 @@ func (fn *formulaFuncs) FV(argsList *list.List) formulaArg { // FVSCHEDULE function calculates the Future Value of an investment with a // variable interest rate. The syntax of the function is: // -// FVSCHEDULE(principal,schedule) -// +// FVSCHEDULE(principal,schedule) func (fn *formulaFuncs) FVSCHEDULE(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "FVSCHEDULE requires 2 arguments") @@ -16306,8 +15913,7 @@ func (fn *formulaFuncs) FVSCHEDULE(argsList *list.List) formulaArg { // INTRATE function calculates the interest rate for a fully invested // security. The syntax of the function is: // -// INTRATE(settlement,maturity,investment,redemption,[basis]) -// +// INTRATE(settlement,maturity,investment,redemption,[basis]) func (fn *formulaFuncs) INTRATE(argsList *list.List) formulaArg { if argsList.Len() != 4 && argsList.Len() != 5 { return newErrorFormulaArg(formulaErrorVALUE, "INTRATE requires 4 or 5 arguments") @@ -16351,8 +15957,7 @@ func (fn *formulaFuncs) INTRATE(argsList *list.List) formulaArg { // loan or investment that is paid in constant periodic payments, with a // constant interest rate. The syntax of the function is: // -// IPMT(rate,per,nper,pv,[fv],[type]) -// +// IPMT(rate,per,nper,pv,[fv],[type]) func (fn *formulaFuncs) IPMT(argsList *list.List) formulaArg { return fn.ipmt("IPMT", argsList) } @@ -16430,8 +16035,7 @@ func (fn *formulaFuncs) ipmt(name string, argsList *list.List) formulaArg { // periodic cash flows (i.e. an initial investment value and a series of net // income values). The syntax of the function is: // -// IRR(values,[guess]) -// +// IRR(values,[guess]) func (fn *formulaFuncs) IRR(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "IRR requires at least 1 argument") @@ -16499,8 +16103,7 @@ func (fn *formulaFuncs) IRR(argsList *list.List) formulaArg { // ISPMT function calculates the interest paid during a specific period of a // loan or investment. The syntax of the function is: // -// ISPMT(rate,per,nper,pv) -// +// ISPMT(rate,per,nper,pv) func (fn *formulaFuncs) ISPMT(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "ISPMT requires 4 arguments") @@ -16536,8 +16139,7 @@ func (fn *formulaFuncs) ISPMT(argsList *list.List) formulaArg { // that pays periodic interest, assuming a par value of $100. The syntax of // the function is: // -// MDURATION(settlement,maturity,coupon,yld,frequency,[basis]) -// +// MDURATION(settlement,maturity,coupon,yld,frequency,[basis]) func (fn *formulaFuncs) MDURATION(argsList *list.List) formulaArg { args := fn.prepareDurationArgs("MDURATION", argsList) if args.Type != ArgList { @@ -16555,8 +16157,7 @@ func (fn *formulaFuncs) MDURATION(argsList *list.List) formulaArg { // initial investment value and a series of net income values). The syntax of // the function is: // -// MIRR(values,finance_rate,reinvest_rate) -// +// MIRR(values,finance_rate,reinvest_rate) func (fn *formulaFuncs) MIRR(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "MIRR requires 3 arguments") @@ -16589,8 +16190,7 @@ func (fn *formulaFuncs) MIRR(argsList *list.List) formulaArg { // interest rate and number of compounding periods per year. The syntax of // the function is: // -// NOMINAL(effect_rate,npery) -// +// NOMINAL(effect_rate,npery) func (fn *formulaFuncs) NOMINAL(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "NOMINAL requires 2 arguments") @@ -16613,8 +16213,7 @@ func (fn *formulaFuncs) NOMINAL(argsList *list.List) formulaArg { // for a constant periodic payment and a constant interest rate. The syntax // of the function is: // -// NPER(rate,pmt,pv,[fv],[type]) -// +// NPER(rate,pmt,pv,[fv],[type]) func (fn *formulaFuncs) NPER(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "NPER requires at least 3 arguments") @@ -16662,8 +16261,7 @@ func (fn *formulaFuncs) NPER(argsList *list.List) formulaArg { // supplied discount rate, and a series of future payments and income. The // syntax of the function is: // -// NPV(rate,value1,[value2],[value3],...) -// +// NPV(rate,value1,[value2],[value3],...) func (fn *formulaFuncs) NPV(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "NPV requires at least 2 arguments") @@ -16827,8 +16425,7 @@ func (fn *formulaFuncs) prepareOddfpriceArgs(argsList *list.List) formulaArg { // ODDFPRICE function calculates the price per $100 face value of a security // with an odd (short or long) first period. The syntax of the function is: // -// ODDFPRICE(settlement,maturity,issue,first_coupon,rate,yld,redemption,frequency,[basis]) -// +// ODDFPRICE(settlement,maturity,issue,first_coupon,rate,yld,redemption,frequency,[basis]) func (fn *formulaFuncs) ODDFPRICE(argsList *list.List) formulaArg { if argsList.Len() != 8 && argsList.Len() != 9 { return newErrorFormulaArg(formulaErrorVALUE, "ODDFPRICE requires 8 or 9 arguments") @@ -16957,8 +16554,7 @@ func (fn *formulaFuncs) ODDFPRICE(argsList *list.List) formulaArg { // investment to reach a specified future value. The syntax of the function // is: // -// PDURATION(rate,pv,fv) -// +// PDURATION(rate,pv,fv) func (fn *formulaFuncs) PDURATION(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "PDURATION requires 3 arguments") @@ -16985,8 +16581,7 @@ func (fn *formulaFuncs) PDURATION(argsList *list.List) formulaArg { // (or partially pay off) a loan or investment, with a constant interest // rate, over a specified period. The syntax of the function is: // -// PMT(rate,nper,pv,[fv],[type]) -// +// PMT(rate,nper,pv,[fv],[type]) func (fn *formulaFuncs) PMT(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "PMT requires at least 3 arguments") @@ -17031,8 +16626,7 @@ func (fn *formulaFuncs) PMT(argsList *list.List) formulaArg { // period of a loan or investment that is paid in constant periodic payments, // with a constant interest rate. The syntax of the function is: // -// PPMT(rate,per,nper,pv,[fv],[type]) -// +// PPMT(rate,per,nper,pv,[fv],[type]) func (fn *formulaFuncs) PPMT(argsList *list.List) formulaArg { return fn.ipmt("PPMT", argsList) } @@ -17073,8 +16667,7 @@ func (fn *formulaFuncs) price(settlement, maturity, rate, yld, redemption, frequ // PRICE function calculates the price, per $100 face value of a security that // pays periodic interest. The syntax of the function is: // -// PRICE(settlement,maturity,rate,yld,redemption,frequency,[basis]) -// +// PRICE(settlement,maturity,rate,yld,redemption,frequency,[basis]) func (fn *formulaFuncs) PRICE(argsList *list.List) formulaArg { if argsList.Len() != 6 && argsList.Len() != 7 { return newErrorFormulaArg(formulaErrorVALUE, "PRICE requires 6 or 7 arguments") @@ -17124,8 +16717,7 @@ func (fn *formulaFuncs) PRICE(argsList *list.List) formulaArg { // PRICEDISC function calculates the price, per $100 face value of a // discounted security. The syntax of the function is: // -// PRICEDISC(settlement,maturity,discount,redemption,[basis]) -// +// PRICEDISC(settlement,maturity,discount,redemption,[basis]) func (fn *formulaFuncs) PRICEDISC(argsList *list.List) formulaArg { if argsList.Len() != 4 && argsList.Len() != 5 { return newErrorFormulaArg(formulaErrorVALUE, "PRICEDISC requires 4 or 5 arguments") @@ -17168,8 +16760,7 @@ func (fn *formulaFuncs) PRICEDISC(argsList *list.List) formulaArg { // PRICEMAT function calculates the price, per $100 face value of a security // that pays interest at maturity. The syntax of the function is: // -// PRICEMAT(settlement,maturity,issue,rate,yld,[basis]) -// +// PRICEMAT(settlement,maturity,issue,rate,yld,[basis]) func (fn *formulaFuncs) PRICEMAT(argsList *list.List) formulaArg { if argsList.Len() != 5 && argsList.Len() != 6 { return newErrorFormulaArg(formulaErrorVALUE, "PRICEMAT requires 5 or 6 arguments") @@ -17217,8 +16808,7 @@ func (fn *formulaFuncs) PRICEMAT(argsList *list.List) formulaArg { // PV function calculates the Present Value of an investment, based on a // series of future payments. The syntax of the function is: // -// PV(rate,nper,pmt,[fv],[type]) -// +// PV(rate,nper,pmt,[fv],[type]) func (fn *formulaFuncs) PV(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "PV requires at least 3 arguments") @@ -17286,8 +16876,7 @@ func (fn *formulaFuncs) rate(nper, pmt, pv, fv, t, guess formulaArg) formulaArg // amount of a loan, or to reach a target amount on an investment, over a // given period. The syntax of the function is: // -// RATE(nper,pmt,pv,[fv],[type],[guess]) -// +// RATE(nper,pmt,pv,[fv],[type],[guess]) func (fn *formulaFuncs) RATE(argsList *list.List) formulaArg { if argsList.Len() < 3 { return newErrorFormulaArg(formulaErrorVALUE, "RATE requires at least 3 arguments") @@ -17334,8 +16923,7 @@ func (fn *formulaFuncs) RATE(argsList *list.List) formulaArg { // RECEIVED function calculates the amount received at maturity for a fully // invested security. The syntax of the function is: // -// RECEIVED(settlement,maturity,investment,discount,[basis]) -// +// RECEIVED(settlement,maturity,investment,discount,[basis]) func (fn *formulaFuncs) RECEIVED(argsList *list.List) formulaArg { if argsList.Len() < 4 { return newErrorFormulaArg(formulaErrorVALUE, "RECEIVED requires at least 4 arguments") @@ -17376,8 +16964,7 @@ func (fn *formulaFuncs) RECEIVED(argsList *list.List) formulaArg { // specified present value, future value and duration. The syntax of the // function is: // -// RRI(nper,pv,fv) -// +// RRI(nper,pv,fv) func (fn *formulaFuncs) RRI(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "RRI requires 3 arguments") @@ -17403,8 +16990,7 @@ func (fn *formulaFuncs) RRI(argsList *list.List) formulaArg { // SLN function calculates the straight line depreciation of an asset for one // period. The syntax of the function is: // -// SLN(cost,salvage,life) -// +// SLN(cost,salvage,life) func (fn *formulaFuncs) SLN(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "SLN requires 3 arguments") @@ -17425,8 +17011,7 @@ func (fn *formulaFuncs) SLN(argsList *list.List) formulaArg { // specified period in the lifetime of an asset. The syntax of the function // is: // -// SYD(cost,salvage,life,per) -// +// SYD(cost,salvage,life,per) func (fn *formulaFuncs) SYD(argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "SYD requires 4 arguments") @@ -17453,8 +17038,7 @@ func (fn *formulaFuncs) SYD(argsList *list.List) formulaArg { // TBILLEQ function calculates the bond-equivalent yield for a Treasury Bill. // The syntax of the function is: // -// TBILLEQ(settlement,maturity,discount) -// +// TBILLEQ(settlement,maturity,discount) func (fn *formulaFuncs) TBILLEQ(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "TBILLEQ requires 3 arguments") @@ -17481,8 +17065,7 @@ func (fn *formulaFuncs) TBILLEQ(argsList *list.List) formulaArg { // TBILLPRICE function returns the price, per $100 face value, of a Treasury // Bill. The syntax of the function is: // -// TBILLPRICE(settlement,maturity,discount) -// +// TBILLPRICE(settlement,maturity,discount) func (fn *formulaFuncs) TBILLPRICE(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "TBILLPRICE requires 3 arguments") @@ -17509,8 +17092,7 @@ func (fn *formulaFuncs) TBILLPRICE(argsList *list.List) formulaArg { // TBILLYIELD function calculates the yield of a Treasury Bill. The syntax of // the function is: // -// TBILLYIELD(settlement,maturity,pr) -// +// TBILLYIELD(settlement,maturity,pr) func (fn *formulaFuncs) TBILLYIELD(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "TBILLYIELD requires 3 arguments") @@ -17625,8 +17207,7 @@ func (fn *formulaFuncs) vdb(cost, salvage, life, life1, period, factor formulaAr // specified period (including partial periods). The syntax of the function // is: // -// VDB(cost,salvage,life,start_period,end_period,[factor],[no_switch]) -// +// VDB(cost,salvage,life,start_period,end_period,[factor],[no_switch]) func (fn *formulaFuncs) VDB(argsList *list.List) formulaArg { if argsList.Len() < 5 || argsList.Len() > 7 { return newErrorFormulaArg(formulaErrorVALUE, "VDB requires 5 or 7 arguments") @@ -17774,8 +17355,7 @@ func xirrPart2(values, dates []float64, rate float64) float64 { // value and a series of net income values) occurring at a series of supplied // dates. The syntax of the function is: // -// XIRR(values,dates,[guess]) -// +// XIRR(values,dates,[guess]) func (fn *formulaFuncs) XIRR(argsList *list.List) formulaArg { if argsList.Len() != 2 && argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "XIRR requires 2 or 3 arguments") @@ -17799,8 +17379,7 @@ func (fn *formulaFuncs) XIRR(argsList *list.List) formulaArg { // XNPV function calculates the Net Present Value for a schedule of cash flows // that is not necessarily periodic. The syntax of the function is: // -// XNPV(rate,values,dates) -// +// XNPV(rate,values,dates) func (fn *formulaFuncs) XNPV(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "XNPV requires 3 arguments") @@ -17862,8 +17441,7 @@ func (fn *formulaFuncs) yield(settlement, maturity, rate, pr, redemption, freque // YIELD function calculates the Yield of a security that pays periodic // interest. The syntax of the function is: // -// YIELD(settlement,maturity,rate,pr,redemption,frequency,[basis]) -// +// YIELD(settlement,maturity,rate,pr,redemption,frequency,[basis]) func (fn *formulaFuncs) YIELD(argsList *list.List) formulaArg { if argsList.Len() != 6 && argsList.Len() != 7 { return newErrorFormulaArg(formulaErrorVALUE, "YIELD requires 6 or 7 arguments") @@ -17913,8 +17491,7 @@ func (fn *formulaFuncs) YIELD(argsList *list.List) formulaArg { // YIELDDISC function calculates the annual yield of a discounted security. // The syntax of the function is: // -// YIELDDISC(settlement,maturity,pr,redemption,[basis]) -// +// YIELDDISC(settlement,maturity,pr,redemption,[basis]) func (fn *formulaFuncs) YIELDDISC(argsList *list.List) formulaArg { if argsList.Len() != 4 && argsList.Len() != 5 { return newErrorFormulaArg(formulaErrorVALUE, "YIELDDISC requires 4 or 5 arguments") @@ -17954,8 +17531,7 @@ func (fn *formulaFuncs) YIELDDISC(argsList *list.List) formulaArg { // YIELDMAT function calculates the annual yield of a security that pays // interest at maturity. The syntax of the function is: // -// YIELDMAT(settlement,maturity,issue,rate,pr,[basis]) -// +// YIELDMAT(settlement,maturity,issue,rate,pr,[basis]) func (fn *formulaFuncs) YIELDMAT(argsList *list.List) formulaArg { if argsList.Len() != 5 && argsList.Len() != 6 { return newErrorFormulaArg(formulaErrorVALUE, "YIELDMAT requires 5 or 6 arguments") @@ -18151,8 +17727,7 @@ func (fn *formulaFuncs) database(name string, argsList *list.List) formulaArg { // field (column) in a database for selected records, that satisfy // user-specified criteria. The syntax of the Excel Daverage function is: // -// DAVERAGE(database,field,criteria) -// +// DAVERAGE(database,field,criteria) func (fn *formulaFuncs) DAVERAGE(argsList *list.List) formulaArg { return fn.database("DAVERAGE", argsList) } @@ -18190,8 +17765,7 @@ func (fn *formulaFuncs) dcount(name string, argsList *list.List) formulaArg { // 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) -// +// DCOUNT(database,[field],criteria) func (fn *formulaFuncs) DCOUNT(argsList *list.List) formulaArg { return fn.dcount("DCOUNT", argsList) } @@ -18201,8 +17775,7 @@ func (fn *formulaFuncs) DCOUNT(argsList *list.List) formulaArg { // 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) -// +// DCOUNTA(database,[field],criteria) func (fn *formulaFuncs) DCOUNTA(argsList *list.List) formulaArg { return fn.dcount("DCOUNTA", argsList) } @@ -18211,8 +17784,7 @@ func (fn *formulaFuncs) DCOUNTA(argsList *list.List) formulaArg { // is selected via a set of one or more user-specified criteria. The syntax of // the function is: // -// DGET(database,field,criteria) -// +// DGET(database,field,criteria) func (fn *formulaFuncs) DGET(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "DGET requires 3 arguments") @@ -18238,8 +17810,7 @@ func (fn *formulaFuncs) DGET(argsList *list.List) formulaArg { // defined by a set of one or more user-specified criteria. The syntax of the // function is: // -// DMAX(database,field,criteria) -// +// DMAX(database,field,criteria) func (fn *formulaFuncs) DMAX(argsList *list.List) formulaArg { return fn.database("DMAX", argsList) } @@ -18249,8 +17820,7 @@ func (fn *formulaFuncs) DMAX(argsList *list.List) formulaArg { // defined by a set of one or more user-specified criteria. The syntax of the // function is: // -// DMIN(database,field,criteria) -// +// DMIN(database,field,criteria) func (fn *formulaFuncs) DMIN(argsList *list.List) formulaArg { return fn.database("DMIN", argsList) } @@ -18259,8 +17829,7 @@ func (fn *formulaFuncs) DMIN(argsList *list.List) formulaArg { // for selected records, that satisfy user-specified criteria. The syntax of // the function is: // -// DPRODUCT(database,field,criteria) -// +// DPRODUCT(database,field,criteria) func (fn *formulaFuncs) DPRODUCT(argsList *list.List) formulaArg { return fn.database("DPRODUCT", argsList) } @@ -18270,8 +17839,7 @@ func (fn *formulaFuncs) DPRODUCT(argsList *list.List) formulaArg { // included in the calculation are defined by a set of one or more // user-specified criteria. The syntax of the function is: // -// DSTDEV(database,field,criteria) -// +// DSTDEV(database,field,criteria) func (fn *formulaFuncs) DSTDEV(argsList *list.List) formulaArg { return fn.database("DSTDEV", argsList) } @@ -18281,8 +17849,7 @@ func (fn *formulaFuncs) DSTDEV(argsList *list.List) formulaArg { // calculation are defined by a set of one or more user-specified criteria. // The syntax of the function is: // -// DSTDEVP(database,field,criteria) -// +// DSTDEVP(database,field,criteria) func (fn *formulaFuncs) DSTDEVP(argsList *list.List) formulaArg { return fn.database("DSTDEVP", argsList) } @@ -18291,8 +17858,7 @@ func (fn *formulaFuncs) DSTDEVP(argsList *list.List) formulaArg { // selected records, that satisfy user-specified criteria. The syntax of the // function is: // -// DSUM(database,field,criteria) -// +// DSUM(database,field,criteria) func (fn *formulaFuncs) DSUM(argsList *list.List) formulaArg { return fn.database("DSUM", argsList) } @@ -18302,8 +17868,7 @@ func (fn *formulaFuncs) DSUM(argsList *list.List) formulaArg { // calculation are defined by a set of one or more user-specified criteria. // The syntax of the function is: // -// DVAR(database,field,criteria) -// +// DVAR(database,field,criteria) func (fn *formulaFuncs) DVAR(argsList *list.List) formulaArg { return fn.database("DVAR", argsList) } @@ -18313,8 +17878,7 @@ func (fn *formulaFuncs) DVAR(argsList *list.List) formulaArg { // records to be included in the calculation are defined by a set of one or // more user-specified criteria. The syntax of the function is: // -// DVARP(database,field,criteria) -// +// DVARP(database,field,criteria) func (fn *formulaFuncs) DVARP(argsList *list.List) formulaArg { return fn.database("DVARP", argsList) } diff --git a/cell.go b/cell.go index 5506189118..214f5c6f89 100644 --- a/cell.go +++ b/cell.go @@ -90,24 +90,24 @@ func (f *File) GetCellType(sheet, axis string) (CellType, error) { // can be set with string text. The following shows the supported data // types: // -// int -// int8 -// int16 -// int32 -// int64 -// uint -// uint8 -// uint16 -// uint32 -// uint64 -// float32 -// float64 -// string -// []byte -// time.Duration -// time.Time -// bool -// nil +// int +// int8 +// int16 +// int32 +// int64 +// uint +// uint8 +// uint16 +// uint32 +// uint64 +// float32 +// float64 +// string +// []byte +// time.Duration +// time.Time +// bool +// nil // // Note that default date format is m/d/yy h:mm of time.Time type value. You // can set numbers format by SetCellStyle() method. If you need to set the @@ -334,9 +334,8 @@ func setCellBool(value bool) (t string, v string) { // represent the number. bitSize is 32 or 64 depending on if a float32 or // float64 was originally used for the value. For Example: // -// var x float32 = 1.325 -// f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) -// +// var x float32 = 1.325 +// f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) func (f *File) SetCellFloat(sheet, axis string, value float64, precision, bitSize int) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -522,73 +521,72 @@ type FormulaOpts struct { // // Example 1, set normal formula "=SUM(A1,B1)" for the cell "A3" on "Sheet1": // -// err := f.SetCellFormula("Sheet1", "A3", "=SUM(A1,B1)") +// err := f.SetCellFormula("Sheet1", "A3", "=SUM(A1,B1)") // // Example 2, set one-dimensional vertical constant array (row array) formula // "1,2,3" for the cell "A3" on "Sheet1": // -// err := f.SetCellFormula("Sheet1", "A3", "={1,2,3}") +// err := f.SetCellFormula("Sheet1", "A3", "={1,2,3}") // // Example 3, set one-dimensional horizontal constant array (column array) // formula '"a","b","c"' for the cell "A3" on "Sheet1": // -// err := f.SetCellFormula("Sheet1", "A3", "={\"a\",\"b\",\"c\"}") +// err := f.SetCellFormula("Sheet1", "A3", "={\"a\",\"b\",\"c\"}") // // Example 4, set two-dimensional constant array formula '{1,2,"a","b"}' for // the cell "A3" on "Sheet1": // -// formulaType, ref := excelize.STCellFormulaTypeArray, "A3:A3" -// err := f.SetCellFormula("Sheet1", "A3", "={1,2,\"a\",\"b\"}", -// excelize.FormulaOpts{Ref: &ref, Type: &formulaType}) +// formulaType, ref := excelize.STCellFormulaTypeArray, "A3:A3" +// err := f.SetCellFormula("Sheet1", "A3", "={1,2,\"a\",\"b\"}", +// excelize.FormulaOpts{Ref: &ref, Type: &formulaType}) // // Example 5, set range array formula "A1:A2" for the cell "A3" on "Sheet1": // -// formulaType, ref := excelize.STCellFormulaTypeArray, "A3:A3" -// err := f.SetCellFormula("Sheet1", "A3", "=A1:A2", -// excelize.FormulaOpts{Ref: &ref, Type: &formulaType}) +// formulaType, ref := excelize.STCellFormulaTypeArray, "A3:A3" +// err := f.SetCellFormula("Sheet1", "A3", "=A1:A2", +// excelize.FormulaOpts{Ref: &ref, Type: &formulaType}) // // Example 6, set shared formula "=A1+B1" for the cell "C1:C5" // on "Sheet1", "C1" is the master cell: // -// formulaType, ref := excelize.STCellFormulaTypeShared, "C1:C5" -// err := f.SetCellFormula("Sheet1", "C1", "=A1+B1", -// excelize.FormulaOpts{Ref: &ref, Type: &formulaType}) +// formulaType, ref := excelize.STCellFormulaTypeShared, "C1:C5" +// err := f.SetCellFormula("Sheet1", "C1", "=A1+B1", +// excelize.FormulaOpts{Ref: &ref, Type: &formulaType}) // // Example 7, set table formula "=SUM(Table1[[A]:[B]])" for the cell "C2" // on "Sheet1": // -// package main -// -// import ( -// "fmt" +// package main // -// "github.com/xuri/excelize/v2" -// ) +// import ( +// "fmt" // -// func main() { -// f := excelize.NewFile() -// for idx, row := range [][]interface{}{{"A", "B", "C"}, {1, 2}} { -// if err := f.SetSheetRow("Sheet1", fmt.Sprintf("A%d", idx+1), &row); err != nil { -// fmt.Println(err) -// return -// } -// } -// if err := f.AddTable("Sheet1", "A1", "C2", -// `{"table_name":"Table1","table_style":"TableStyleMedium2"}`); err != nil { -// fmt.Println(err) -// return -// } -// formulaType := excelize.STCellFormulaTypeDataTable -// if err := f.SetCellFormula("Sheet1", "C2", "=SUM(Table1[[A]:[B]])", -// excelize.FormulaOpts{Type: &formulaType}); err != nil { -// fmt.Println(err) -// return -// } -// if err := f.SaveAs("Book1.xlsx"); err != nil { -// fmt.Println(err) -// } -// } +// "github.com/xuri/excelize/v2" +// ) // +// func main() { +// f := excelize.NewFile() +// for idx, row := range [][]interface{}{{"A", "B", "C"}, {1, 2}} { +// if err := f.SetSheetRow("Sheet1", fmt.Sprintf("A%d", idx+1), &row); err != nil { +// fmt.Println(err) +// return +// } +// } +// if err := f.AddTable("Sheet1", "A1", "C2", +// `{"table_name":"Table1","table_style":"TableStyleMedium2"}`); err != nil { +// fmt.Println(err) +// return +// } +// formulaType := excelize.STCellFormulaTypeDataTable +// if err := f.SetCellFormula("Sheet1", "C2", "=SUM(Table1[[A]:[B]])", +// excelize.FormulaOpts{Type: &formulaType}); err != nil { +// fmt.Println(err) +// return +// } +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// fmt.Println(err) +// } +// } func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -671,8 +669,7 @@ func (ws *xlsxWorksheet) countSharedFormula() (count int) { // // For example, get a hyperlink to a 'H6' cell on a worksheet named 'Sheet1': // -// link, target, err := f.GetCellHyperLink("Sheet1", "H6") -// +// link, target, err := f.GetCellHyperLink("Sheet1", "H6") func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) { // Check for correct cell name if _, _, err := SplitCellName(axis); err != nil { @@ -714,27 +711,26 @@ type HyperlinkOpts struct { // the other functions such as `SetCellStyle` or `SetSheetRow`. The below is // example for external link. // -// display, tooltip := "https://github.com/xuri/excelize", "Excelize on GitHub" -// if err := f.SetCellHyperLink("Sheet1", "A3", -// "https://github.com/xuri/excelize", "External", excelize.HyperlinkOpts{ -// Display: &display, -// Tooltip: &tooltip, -// }); err != nil { -// fmt.Println(err) -// } -// // Set underline and font color style for the cell. -// style, err := f.NewStyle(&excelize.Style{ -// Font: &excelize.Font{Color: "#1265BE", Underline: "single"}, -// }) -// if err != nil { -// fmt.Println(err) -// } -// err = f.SetCellStyle("Sheet1", "A3", "A3", style) +// display, tooltip := "https://github.com/xuri/excelize", "Excelize on GitHub" +// if err := f.SetCellHyperLink("Sheet1", "A3", +// "https://github.com/xuri/excelize", "External", excelize.HyperlinkOpts{ +// Display: &display, +// Tooltip: &tooltip, +// }); err != nil { +// fmt.Println(err) +// } +// // Set underline and font color style for the cell. +// style, err := f.NewStyle(&excelize.Style{ +// Font: &excelize.Font{Color: "#1265BE", Underline: "single"}, +// }) +// if err != nil { +// fmt.Println(err) +// } +// err = f.SetCellStyle("Sheet1", "A3", "A3", style) // // This is another example for "Location": // -// err := f.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") -// +// err := f.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...HyperlinkOpts) error { // Check for correct cell name if _, _, err := SplitCellName(axis); err != nil { @@ -892,121 +888,120 @@ func newRpr(fnt *Font) *xlsxRPr { // worksheet. For example, set rich text on the A1 cell of the worksheet named // Sheet1: // -// package main -// -// import ( -// "fmt" +// package main // -// "github.com/xuri/excelize/v2" -// ) +// import ( +// "fmt" // -// func main() { -// f := excelize.NewFile() -// if err := f.SetRowHeight("Sheet1", 1, 35); err != nil { -// fmt.Println(err) -// return -// } -// if err := f.SetColWidth("Sheet1", "A", "A", 44); err != nil { -// fmt.Println(err) -// return -// } -// if err := f.SetCellRichText("Sheet1", "A1", []excelize.RichTextRun{ -// { -// Text: "bold", -// Font: &excelize.Font{ -// Bold: true, -// Color: "2354e8", -// Family: "Times New Roman", -// }, -// }, -// { -// Text: " and ", -// Font: &excelize.Font{ -// Family: "Times New Roman", -// }, -// }, -// { -// Text: "italic ", -// Font: &excelize.Font{ -// Bold: true, -// Color: "e83723", -// Italic: true, -// Family: "Times New Roman", -// }, -// }, -// { -// Text: "text with color and font-family,", -// Font: &excelize.Font{ -// Bold: true, -// Color: "2354e8", -// Family: "Times New Roman", -// }, -// }, -// { -// Text: "\r\nlarge text with ", -// Font: &excelize.Font{ -// Size: 14, -// Color: "ad23e8", -// }, -// }, -// { -// Text: "strike", -// Font: &excelize.Font{ -// Color: "e89923", -// Strike: true, -// }, -// }, -// { -// Text: " superscript", -// Font: &excelize.Font{ -// Color: "dbc21f", -// VertAlign: "superscript", -// }, -// }, -// { -// Text: " and ", -// Font: &excelize.Font{ -// Size: 14, -// Color: "ad23e8", -// VertAlign: "baseline", -// }, -// }, -// { -// Text: "underline", -// Font: &excelize.Font{ -// Color: "23e833", -// Underline: "single", -// }, -// }, -// { -// Text: " subscript.", -// Font: &excelize.Font{ -// Color: "017505", -// VertAlign: "subscript", -// }, -// }, -// }); err != nil { -// fmt.Println(err) -// return -// } -// style, err := f.NewStyle(&excelize.Style{ -// Alignment: &excelize.Alignment{ -// WrapText: true, -// }, -// }) -// if err != nil { -// fmt.Println(err) -// return -// } -// if err := f.SetCellStyle("Sheet1", "A1", "A1", style); err != nil { -// fmt.Println(err) -// return -// } -// if err := f.SaveAs("Book1.xlsx"); err != nil { -// fmt.Println(err) -// } -// } +// "github.com/xuri/excelize/v2" +// ) // +// func main() { +// f := excelize.NewFile() +// if err := f.SetRowHeight("Sheet1", 1, 35); err != nil { +// fmt.Println(err) +// return +// } +// if err := f.SetColWidth("Sheet1", "A", "A", 44); err != nil { +// fmt.Println(err) +// return +// } +// if err := f.SetCellRichText("Sheet1", "A1", []excelize.RichTextRun{ +// { +// Text: "bold", +// Font: &excelize.Font{ +// Bold: true, +// Color: "2354e8", +// Family: "Times New Roman", +// }, +// }, +// { +// Text: " and ", +// Font: &excelize.Font{ +// Family: "Times New Roman", +// }, +// }, +// { +// Text: "italic ", +// Font: &excelize.Font{ +// Bold: true, +// Color: "e83723", +// Italic: true, +// Family: "Times New Roman", +// }, +// }, +// { +// Text: "text with color and font-family,", +// Font: &excelize.Font{ +// Bold: true, +// Color: "2354e8", +// Family: "Times New Roman", +// }, +// }, +// { +// Text: "\r\nlarge text with ", +// Font: &excelize.Font{ +// Size: 14, +// Color: "ad23e8", +// }, +// }, +// { +// Text: "strike", +// Font: &excelize.Font{ +// Color: "e89923", +// Strike: true, +// }, +// }, +// { +// Text: " superscript", +// Font: &excelize.Font{ +// Color: "dbc21f", +// VertAlign: "superscript", +// }, +// }, +// { +// Text: " and ", +// Font: &excelize.Font{ +// Size: 14, +// Color: "ad23e8", +// VertAlign: "baseline", +// }, +// }, +// { +// Text: "underline", +// Font: &excelize.Font{ +// Color: "23e833", +// Underline: "single", +// }, +// }, +// { +// Text: " subscript.", +// Font: &excelize.Font{ +// Color: "017505", +// VertAlign: "subscript", +// }, +// }, +// }); err != nil { +// fmt.Println(err) +// return +// } +// style, err := f.NewStyle(&excelize.Style{ +// Alignment: &excelize.Alignment{ +// WrapText: true, +// }, +// }) +// if err != nil { +// fmt.Println(err) +// return +// } +// if err := f.SetCellStyle("Sheet1", "A1", "A1", style); err != nil { +// fmt.Println(err) +// return +// } +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// fmt.Println(err) +// } +// } func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -1055,8 +1050,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { // coordinate and a pointer to array type 'slice'. For example, writes an // array to row 6 start with the cell B6 on Sheet1: // -// err := f.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) -// +// err := f.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { col, row, err := CellNameToCoordinates(axis) if err != nil { diff --git a/chart.go b/chart.go index 7dcbe19bec..e18545d773 100644 --- a/chart.go +++ b/chart.go @@ -500,152 +500,152 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // properties set. For example, create 3D clustered column chart with data // Sheet1!$E$1:$L$15: // -// package main +// package main // -// import ( -// "fmt" +// import ( +// "fmt" // -// "github.com/xuri/excelize/v2" -// ) +// "github.com/xuri/excelize/v2" +// ) // -// func main() { -// categories := map[string]string{ -// "A2": "Small", "A3": "Normal", "A4": "Large", -// "B1": "Apple", "C1": "Orange", "D1": "Pear"} -// values := map[string]int{ -// "B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8} -// f := excelize.NewFile() -// for k, v := range categories { -// f.SetCellValue("Sheet1", k, v) -// } -// for k, v := range values { -// f.SetCellValue("Sheet1", k, v) -// } -// if err := f.AddChart("Sheet1", "E1", `{ -// "type": "col3DClustered", -// "series": [ -// { -// "name": "Sheet1!$A$2", -// "categories": "Sheet1!$B$1:$D$1", -// "values": "Sheet1!$B$2:$D$2" -// }, -// { -// "name": "Sheet1!$A$3", -// "categories": "Sheet1!$B$1:$D$1", -// "values": "Sheet1!$B$3:$D$3" -// }, -// { -// "name": "Sheet1!$A$4", -// "categories": "Sheet1!$B$1:$D$1", -// "values": "Sheet1!$B$4:$D$4" -// }], -// "title": -// { -// "name": "Fruit 3D Clustered Column Chart" -// }, -// "legend": -// { -// "none": false, -// "position": "bottom", -// "show_legend_key": false -// }, -// "plotarea": -// { -// "show_bubble_size": true, -// "show_cat_name": false, -// "show_leader_lines": false, -// "show_percent": true, -// "show_series_name": true, -// "show_val": true -// }, -// "show_blanks_as": "zero", -// "x_axis": -// { -// "reverse_order": true -// }, -// "y_axis": -// { -// "maximum": 7.5, -// "minimum": 0.5 -// } -// }`); err != nil { -// fmt.Println(err) -// return -// } -// // Save spreadsheet by the given path. -// if err := f.SaveAs("Book1.xlsx"); err != nil { -// fmt.Println(err) -// } -// } +// func main() { +// categories := map[string]string{ +// "A2": "Small", "A3": "Normal", "A4": "Large", +// "B1": "Apple", "C1": "Orange", "D1": "Pear"} +// values := map[string]int{ +// "B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8} +// f := excelize.NewFile() +// for k, v := range categories { +// f.SetCellValue("Sheet1", k, v) +// } +// for k, v := range values { +// f.SetCellValue("Sheet1", k, v) +// } +// if err := f.AddChart("Sheet1", "E1", `{ +// "type": "col3DClustered", +// "series": [ +// { +// "name": "Sheet1!$A$2", +// "categories": "Sheet1!$B$1:$D$1", +// "values": "Sheet1!$B$2:$D$2" +// }, +// { +// "name": "Sheet1!$A$3", +// "categories": "Sheet1!$B$1:$D$1", +// "values": "Sheet1!$B$3:$D$3" +// }, +// { +// "name": "Sheet1!$A$4", +// "categories": "Sheet1!$B$1:$D$1", +// "values": "Sheet1!$B$4:$D$4" +// }], +// "title": +// { +// "name": "Fruit 3D Clustered Column Chart" +// }, +// "legend": +// { +// "none": false, +// "position": "bottom", +// "show_legend_key": false +// }, +// "plotarea": +// { +// "show_bubble_size": true, +// "show_cat_name": false, +// "show_leader_lines": false, +// "show_percent": true, +// "show_series_name": true, +// "show_val": true +// }, +// "show_blanks_as": "zero", +// "x_axis": +// { +// "reverse_order": true +// }, +// "y_axis": +// { +// "maximum": 7.5, +// "minimum": 0.5 +// } +// }`); err != nil { +// fmt.Println(err) +// return +// } +// // Save spreadsheet by the given path. +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// fmt.Println(err) +// } +// } // // The following shows the type of chart supported by excelize: // -// Type | Chart -// -----------------------------+------------------------------ -// area | 2D area chart -// areaStacked | 2D stacked area chart -// areaPercentStacked | 2D 100% stacked area chart -// area3D | 3D area chart -// area3DStacked | 3D stacked area chart -// area3DPercentStacked | 3D 100% stacked area chart -// bar | 2D clustered bar chart -// barStacked | 2D stacked bar chart -// barPercentStacked | 2D 100% stacked bar chart -// bar3DClustered | 3D clustered bar chart -// bar3DStacked | 3D stacked bar chart -// bar3DPercentStacked | 3D 100% stacked bar chart -// bar3DConeClustered | 3D cone clustered bar chart -// bar3DConeStacked | 3D cone stacked bar chart -// bar3DConePercentStacked | 3D cone percent bar chart -// bar3DPyramidClustered | 3D pyramid clustered bar chart -// bar3DPyramidStacked | 3D pyramid stacked bar chart -// bar3DPyramidPercentStacked | 3D pyramid percent stacked bar chart -// bar3DCylinderClustered | 3D cylinder clustered bar chart -// bar3DCylinderStacked | 3D cylinder stacked bar chart -// bar3DCylinderPercentStacked | 3D cylinder percent stacked bar chart -// col | 2D clustered column chart -// colStacked | 2D stacked column chart -// colPercentStacked | 2D 100% stacked column chart -// col3DClustered | 3D clustered column chart -// col3D | 3D column chart -// col3DStacked | 3D stacked column chart -// col3DPercentStacked | 3D 100% stacked column chart -// col3DCone | 3D cone column chart -// col3DConeClustered | 3D cone clustered column chart -// col3DConeStacked | 3D cone stacked column chart -// col3DConePercentStacked | 3D cone percent stacked column chart -// col3DPyramid | 3D pyramid column chart -// col3DPyramidClustered | 3D pyramid clustered column chart -// col3DPyramidStacked | 3D pyramid stacked column chart -// col3DPyramidPercentStacked | 3D pyramid percent stacked column chart -// col3DCylinder | 3D cylinder column chart -// col3DCylinderClustered | 3D cylinder clustered column chart -// col3DCylinderStacked | 3D cylinder stacked column chart -// col3DCylinderPercentStacked | 3D cylinder percent stacked column chart -// doughnut | doughnut chart -// line | line chart -// pie | pie chart -// pie3D | 3D pie chart -// pieOfPie | pie of pie chart -// barOfPie | bar of pie chart -// radar | radar chart -// scatter | scatter chart -// surface3D | 3D surface chart -// wireframeSurface3D | 3D wireframe surface chart -// contour | contour chart -// wireframeContour | wireframe contour chart -// bubble | bubble chart -// bubble3D | 3D bubble chart +// Type | Chart +// -----------------------------+------------------------------ +// area | 2D area chart +// areaStacked | 2D stacked area chart +// areaPercentStacked | 2D 100% stacked area chart +// area3D | 3D area chart +// area3DStacked | 3D stacked area chart +// area3DPercentStacked | 3D 100% stacked area chart +// bar | 2D clustered bar chart +// barStacked | 2D stacked bar chart +// barPercentStacked | 2D 100% stacked bar chart +// bar3DClustered | 3D clustered bar chart +// bar3DStacked | 3D stacked bar chart +// bar3DPercentStacked | 3D 100% stacked bar chart +// bar3DConeClustered | 3D cone clustered bar chart +// bar3DConeStacked | 3D cone stacked bar chart +// bar3DConePercentStacked | 3D cone percent bar chart +// bar3DPyramidClustered | 3D pyramid clustered bar chart +// bar3DPyramidStacked | 3D pyramid stacked bar chart +// bar3DPyramidPercentStacked | 3D pyramid percent stacked bar chart +// bar3DCylinderClustered | 3D cylinder clustered bar chart +// bar3DCylinderStacked | 3D cylinder stacked bar chart +// bar3DCylinderPercentStacked | 3D cylinder percent stacked bar chart +// col | 2D clustered column chart +// colStacked | 2D stacked column chart +// colPercentStacked | 2D 100% stacked column chart +// col3DClustered | 3D clustered column chart +// col3D | 3D column chart +// col3DStacked | 3D stacked column chart +// col3DPercentStacked | 3D 100% stacked column chart +// col3DCone | 3D cone column chart +// col3DConeClustered | 3D cone clustered column chart +// col3DConeStacked | 3D cone stacked column chart +// col3DConePercentStacked | 3D cone percent stacked column chart +// col3DPyramid | 3D pyramid column chart +// col3DPyramidClustered | 3D pyramid clustered column chart +// col3DPyramidStacked | 3D pyramid stacked column chart +// col3DPyramidPercentStacked | 3D pyramid percent stacked column chart +// col3DCylinder | 3D cylinder column chart +// col3DCylinderClustered | 3D cylinder clustered column chart +// col3DCylinderStacked | 3D cylinder stacked column chart +// col3DCylinderPercentStacked | 3D cylinder percent stacked column chart +// doughnut | doughnut chart +// line | line chart +// pie | pie chart +// pie3D | 3D pie chart +// pieOfPie | pie of pie chart +// barOfPie | bar of pie chart +// radar | radar chart +// scatter | scatter chart +// surface3D | 3D surface chart +// wireframeSurface3D | 3D wireframe surface chart +// contour | contour chart +// wireframeContour | wireframe contour chart +// bubble | bubble chart +// bubble3D | 3D bubble chart // // In Excel a chart series is a collection of information that defines which data is plotted such as values, axis labels and formatting. // // The series options that can be set are: // -// name -// categories -// values -// line -// marker +// name +// categories +// values +// line +// marker // // name: Set the name for the series. The name is displayed in the chart legend and in the formula bar. The name property is optional and if it isn't supplied it will default to Series 1..n. The name can also be a formula such as Sheet1!$A$1 // @@ -657,48 +657,48 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // marker: This sets the marker of the line chart and scatter chart. The range of optional field 'size' is 2-72 (default value is 5). The enumeration value of optional field 'symbol' are (default value is 'auto'): // -// circle -// dash -// diamond -// dot -// none -// picture -// plus -// square -// star -// triangle -// x -// auto +// circle +// dash +// diamond +// dot +// none +// picture +// plus +// square +// star +// triangle +// x +// auto // // Set properties of the chart legend. The options that can be set are: // -// none -// position -// show_legend_key +// none +// position +// show_legend_key // // none: Specified if show the legend without overlapping the chart. The default value is 'false'. // // position: Set the position of the chart legend. The default legend position is right. This parameter only takes effect when 'none' is false. The available positions are: // -// top -// bottom -// left -// right -// top_right +// top +// bottom +// left +// right +// top_right // // show_legend_key: Set the legend keys shall be shown in data labels. The default value is false. // // Set properties of the chart title. The properties that can be set are: // -// title +// title // // name: Set the name (title) for the chart. The name is displayed above the chart. The name can also be a formula such as Sheet1!$A$1 or a list with a sheetname. The name property is optional. The default is to have no chart title. // // Specifies how blank cells are plotted on the chart by show_blanks_as. The default value is gap. The options that can be set are: // -// gap -// span -// zero +// gap +// span +// zero // // gap: Specifies that blank values shall be left as a gap. // @@ -712,12 +712,12 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // Set the position of the chart plot area by plotarea. The properties that can be set are: // -// show_bubble_size -// show_cat_name -// show_leader_lines -// show_percent -// show_series_name -// show_val +// show_bubble_size +// show_cat_name +// show_leader_lines +// show_percent +// show_series_name +// show_val // // show_bubble_size: Specifies the bubble size shall be shown in a data label. The show_bubble_size property is optional. The default value is false. // @@ -733,23 +733,23 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // Set the primary horizontal and vertical axis options by x_axis and y_axis. The properties of x_axis that can be set are: // -// none -// major_grid_lines -// minor_grid_lines -// tick_label_skip -// reverse_order -// maximum -// minimum +// none +// major_grid_lines +// minor_grid_lines +// tick_label_skip +// reverse_order +// maximum +// minimum // // The properties of y_axis that can be set are: // -// none -// major_grid_lines -// minor_grid_lines -// major_unit -// reverse_order -// maximum -// minimum +// none +// major_grid_lines +// minor_grid_lines +// major_unit +// reverse_order +// maximum +// minimum // // none: Disable axes. // @@ -773,115 +773,114 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // in a single chart. For example, create a clustered column - line chart with // data Sheet1!$E$1:$L$15: // -// package main +// package main // -// import ( -// "fmt" +// import ( +// "fmt" // -// "github.com/xuri/excelize/v2" -// ) -// -// func main() { -// categories := map[string]string{ -// "A2": "Small", "A3": "Normal", "A4": "Large", -// "B1": "Apple", "C1": "Orange", "D1": "Pear"} -// values := map[string]int{ -// "B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8} -// f := excelize.NewFile() -// for k, v := range categories { -// f.SetCellValue("Sheet1", k, v) -// } -// for k, v := range values { -// f.SetCellValue("Sheet1", k, v) -// } -// if err := f.AddChart("Sheet1", "E1", `{ -// "type": "col", -// "series": [ -// { -// "name": "Sheet1!$A$2", -// "categories": "", -// "values": "Sheet1!$B$2:$D$2" -// }, -// { -// "name": "Sheet1!$A$3", -// "categories": "Sheet1!$B$1:$D$1", -// "values": "Sheet1!$B$3:$D$3" -// }], -// "format": -// { -// "x_scale": 1.0, -// "y_scale": 1.0, -// "x_offset": 15, -// "y_offset": 10, -// "print_obj": true, -// "lock_aspect_ratio": false, -// "locked": false -// }, -// "title": -// { -// "name": "Clustered Column - Line Chart" -// }, -// "legend": -// { -// "position": "left", -// "show_legend_key": false -// }, -// "plotarea": -// { -// "show_bubble_size": true, -// "show_cat_name": false, -// "show_leader_lines": false, -// "show_percent": true, -// "show_series_name": true, -// "show_val": true -// } -// }`, `{ -// "type": "line", -// "series": [ -// { -// "name": "Sheet1!$A$4", -// "categories": "Sheet1!$B$1:$D$1", -// "values": "Sheet1!$B$4:$D$4", -// "marker": -// { -// "symbol": "none", -// "size": 10 -// } -// }], -// "format": -// { -// "x_scale": 1, -// "y_scale": 1, -// "x_offset": 15, -// "y_offset": 10, -// "print_obj": true, -// "lock_aspect_ratio": false, -// "locked": false -// }, -// "legend": -// { -// "position": "right", -// "show_legend_key": false -// }, -// "plotarea": -// { -// "show_bubble_size": true, -// "show_cat_name": false, -// "show_leader_lines": false, -// "show_percent": true, -// "show_series_name": true, -// "show_val": true -// } -// }`); err != nil { -// fmt.Println(err) -// return -// } -// // Save spreadsheet file by the given path. -// if err := f.SaveAs("Book1.xlsx"); err != nil { -// fmt.Println(err) -// } -// } +// "github.com/xuri/excelize/v2" +// ) // +// func main() { +// categories := map[string]string{ +// "A2": "Small", "A3": "Normal", "A4": "Large", +// "B1": "Apple", "C1": "Orange", "D1": "Pear"} +// values := map[string]int{ +// "B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8} +// f := excelize.NewFile() +// for k, v := range categories { +// f.SetCellValue("Sheet1", k, v) +// } +// for k, v := range values { +// f.SetCellValue("Sheet1", k, v) +// } +// if err := f.AddChart("Sheet1", "E1", `{ +// "type": "col", +// "series": [ +// { +// "name": "Sheet1!$A$2", +// "categories": "", +// "values": "Sheet1!$B$2:$D$2" +// }, +// { +// "name": "Sheet1!$A$3", +// "categories": "Sheet1!$B$1:$D$1", +// "values": "Sheet1!$B$3:$D$3" +// }], +// "format": +// { +// "x_scale": 1.0, +// "y_scale": 1.0, +// "x_offset": 15, +// "y_offset": 10, +// "print_obj": true, +// "lock_aspect_ratio": false, +// "locked": false +// }, +// "title": +// { +// "name": "Clustered Column - Line Chart" +// }, +// "legend": +// { +// "position": "left", +// "show_legend_key": false +// }, +// "plotarea": +// { +// "show_bubble_size": true, +// "show_cat_name": false, +// "show_leader_lines": false, +// "show_percent": true, +// "show_series_name": true, +// "show_val": true +// } +// }`, `{ +// "type": "line", +// "series": [ +// { +// "name": "Sheet1!$A$4", +// "categories": "Sheet1!$B$1:$D$1", +// "values": "Sheet1!$B$4:$D$4", +// "marker": +// { +// "symbol": "none", +// "size": 10 +// } +// }], +// "format": +// { +// "x_scale": 1, +// "y_scale": 1, +// "x_offset": 15, +// "y_offset": 10, +// "print_obj": true, +// "lock_aspect_ratio": false, +// "locked": false +// }, +// "legend": +// { +// "position": "right", +// "show_legend_key": false +// }, +// "plotarea": +// { +// "show_bubble_size": true, +// "show_cat_name": false, +// "show_leader_lines": false, +// "show_percent": true, +// "show_series_name": true, +// "show_val": true +// } +// }`); err != nil { +// fmt.Println(err) +// return +// } +// // Save spreadsheet file by the given path. +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// fmt.Println(err) +// } +// } func (f *File) AddChart(sheet, cell, format string, combo ...string) error { // Read sheet data. ws, err := f.workSheetReader(sheet) diff --git a/col.go b/col.go index 95c7961d35..248e22c27b 100644 --- a/col.go +++ b/col.go @@ -50,18 +50,17 @@ type Cols struct { // worksheet named // 'Sheet1': // -// cols, err := f.GetCols("Sheet1") -// if err != nil { -// fmt.Println(err) -// return -// } -// for _, col := range cols { -// for _, rowCell := range col { -// fmt.Print(rowCell, "\t") -// } -// fmt.Println() -// } -// +// cols, err := f.GetCols("Sheet1") +// if err != nil { +// fmt.Println(err) +// return +// } +// for _, col := range cols { +// for _, rowCell := range col { +// fmt.Print(rowCell, "\t") +// } +// fmt.Println() +// } func (f *File) GetCols(sheet string, opts ...Options) ([][]string, error) { cols, err := f.Cols(sheet) if err != nil { @@ -187,22 +186,21 @@ func columnXMLHandler(colIterator *columnXMLIterator, xmlElement *xml.StartEleme // Cols returns a columns iterator, used for streaming reading data for a // worksheet with a large data. For example: // -// cols, err := f.Cols("Sheet1") -// if err != nil { -// fmt.Println(err) -// return -// } -// for cols.Next() { -// col, err := cols.Rows() -// if err != nil { -// fmt.Println(err) -// } -// for _, rowCell := range col { -// fmt.Print(rowCell, "\t") -// } -// fmt.Println() -// } -// +// cols, err := f.Cols("Sheet1") +// if err != nil { +// fmt.Println(err) +// return +// } +// for cols.Next() { +// col, err := cols.Rows() +// if err != nil { +// fmt.Println(err) +// } +// for _, rowCell := range col { +// fmt.Print(rowCell, "\t") +// } +// fmt.Println() +// } func (f *File) Cols(sheet string) (*Cols, error) { name, ok := f.getSheetXMLPath(sheet) if !ok { @@ -244,8 +242,7 @@ func (f *File) Cols(sheet string) (*Cols, error) { // worksheet name and column name. For example, get visible state of column D // in Sheet1: // -// visible, err := f.GetColVisible("Sheet1", "D") -// +// visible, err := f.GetColVisible("Sheet1", "D") func (f *File) GetColVisible(sheet, col string) (bool, error) { colNum, err := ColumnNameToNumber(col) if err != nil { @@ -273,12 +270,11 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { // // For example hide column D on Sheet1: // -// err := f.SetColVisible("Sheet1", "D", false) +// err := f.SetColVisible("Sheet1", "D", false) // // Hide the columns from D to F (included): // -// err := f.SetColVisible("Sheet1", "D:F", false) -// +// err := f.SetColVisible("Sheet1", "D:F", false) func (f *File) SetColVisible(sheet, columns string, visible bool) error { start, end, err := f.parseColRange(columns) if err != nil { @@ -318,8 +314,7 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error { // column by given worksheet name and column name. For example, get outline // level of column D in Sheet1: // -// level, err := f.GetColOutlineLevel("Sheet1", "D") -// +// level, err := f.GetColOutlineLevel("Sheet1", "D") func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { level := uint8(0) colNum, err := ColumnNameToNumber(col) @@ -365,8 +360,7 @@ func (f *File) parseColRange(columns string) (start, end int, err error) { // column by given worksheet name and column name. The value of parameter // 'level' is 1-7. For example, set outline level of column D in Sheet1 to 2: // -// err := f.SetColOutlineLevel("Sheet1", "D", 2) -// +// err := f.SetColOutlineLevel("Sheet1", "D", 2) func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { if level > 7 || level < 1 { return ErrOutlineLevel @@ -411,12 +405,11 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { // // For example set style of column H on Sheet1: // -// err = f.SetColStyle("Sheet1", "H", style) +// err = f.SetColStyle("Sheet1", "H", style) // // Set style of columns C:F on Sheet1: // -// err = f.SetColStyle("Sheet1", "C:F", style) -// +// err = f.SetColStyle("Sheet1", "C:F", style) func (f *File) SetColStyle(sheet, columns string, styleID int) error { start, end, err := f.parseColRange(columns) if err != nil { @@ -457,9 +450,8 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { // SetColWidth provides a function to set the width of a single column or // multiple columns. For example: // -// f := excelize.NewFile() -// err := f.SetColWidth("Sheet1", "A", "H", 20) -// +// f := excelize.NewFile() +// err := f.SetColWidth("Sheet1", "A", "H", 20) func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error { min, err := ColumnNameToNumber(startCol) if err != nil { @@ -538,25 +530,25 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) // positionObjectPixels calculate the vertices that define the position of a // graphical object within the worksheet in pixels. // -// +------------+------------+ -// | A | B | -// +-----+------------+------------+ -// | |(x1,y1) | | -// | 1 |(A1)._______|______ | -// | | | | | -// | | | | | -// +-----+----| OBJECT |-----+ -// | | | | | -// | 2 | |______________. | -// | | | (B2)| -// | | | (x2,y2)| -// +-----+------------+------------+ +// +------------+------------+ +// | A | B | +// +-----+------------+------------+ +// | |(x1,y1) | | +// | 1 |(A1)._______|______ | +// | | | | | +// | | | | | +// +-----+----| OBJECT |-----+ +// | | | | | +// | 2 | |______________. | +// | | | (B2)| +// | | | (x2,y2)| +// +-----+------------+------------+ // // Example of an object that covers some area from cell A1 to B2. // // Based on the width and height of the object we need to calculate 8 vars: // -// colStart, rowStart, colEnd, rowEnd, x1, y1, x2, y2. +// colStart, rowStart, colEnd, rowEnd, x1, y1, x2, y2. // // We also calculate the absolute x and y position of the top left vertex of // the object. This is required for images. @@ -569,21 +561,20 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) // subtracting the width and height of the object from the width and // height of the underlying cells. // -// colStart # Col containing upper left corner of object. -// x1 # Distance to left side of object. +// colStart # Col containing upper left corner of object. +// x1 # Distance to left side of object. // -// rowStart # Row containing top left corner of object. -// y1 # Distance to top of object. +// rowStart # Row containing top left corner of object. +// y1 # Distance to top of object. // -// colEnd # Col containing lower right corner of object. -// x2 # Distance to right side of object. +// colEnd # Col containing lower right corner of object. +// x2 # Distance to right side of object. // -// rowEnd # Row containing bottom right corner of object. -// y2 # Distance to bottom of object. -// -// width # Width of object frame. -// height # Height of object frame. +// rowEnd # Row containing bottom right corner of object. +// y2 # Distance to bottom of object. // +// width # Width of object frame. +// height # Height of object frame. func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int) { // Adjust start column for offsets that are greater than the col width. for x1 >= f.getColWidth(sheet, col) { @@ -669,8 +660,7 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { // InsertCol provides a function to insert a new column before given column // index. For example, create a new column before column C in Sheet1: // -// err := f.InsertCol("Sheet1", "C") -// +// err := f.InsertCol("Sheet1", "C") func (f *File) InsertCol(sheet, col string) error { num, err := ColumnNameToNumber(col) if err != nil { @@ -682,7 +672,7 @@ func (f *File) InsertCol(sheet, col string) error { // RemoveCol provides a function to remove single column by given worksheet // name and column index. For example, remove column C in Sheet1: // -// err := f.RemoveCol("Sheet1", "C") +// err := f.RemoveCol("Sheet1", "C") // // Use this method with caution, which will affect changes in references such // as formulas, charts, and so on. If there is any referenced value of the diff --git a/comment.go b/comment.go index 03b12155ec..0794986156 100644 --- a/comment.go +++ b/comment.go @@ -92,8 +92,7 @@ func (f *File) getSheetComments(sheetFile string) string { // author length is 255 and the max text length is 32512. For example, add a // comment in Sheet1!$A$30: // -// err := f.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`) -// +// err := f.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`) func (f *File) AddComment(sheet, cell, format string) error { formatSet, err := parseFormatCommentsSet(format) if err != nil { diff --git a/crypt.go b/crypt.go index b00ccdf7b3..a5670ac2fd 100644 --- a/crypt.go +++ b/crypt.go @@ -1175,10 +1175,9 @@ func (c *cfb) writeSAT(MSATBlocks, SATBlocks, SSATBlocks, directoryBlocks, fileB // Writer provides a function to create compound file with given info stream // and package stream. // -// MSAT - The master sector allocation table -// SSAT - The short sector allocation table -// SAT - The sector allocation table -// +// MSAT - The master sector allocation table +// SSAT - The short sector allocation table +// SAT - The sector allocation table func (c *cfb) Writer(encryptionInfoBuffer, encryptedPackage []byte) []byte { var ( storage cfb diff --git a/datavalidation.go b/datavalidation.go index 1b06b6a7e2..0cad1b8be9 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -166,11 +166,10 @@ func (dd *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o D // Sheet1!A7:B8 with validation criteria source Sheet1!E1:E3 settings, create // in-cell dropdown by allowing list source: // -// dvRange := excelize.NewDataValidation(true) -// dvRange.Sqref = "A7:B8" -// dvRange.SetSqrefDropList("$E$1:$E$3") -// f.AddDataValidation("Sheet1", dvRange) -// +// dvRange := excelize.NewDataValidation(true) +// dvRange.Sqref = "A7:B8" +// dvRange.SetSqrefDropList("$E$1:$E$3") +// f.AddDataValidation("Sheet1", dvRange) func (dd *DataValidation) SetSqrefDropList(sqref string) { dd.Formula1 = fmt.Sprintf("%s", sqref) dd.Type = convDataValidationType(typeList) @@ -225,29 +224,28 @@ func convDataValidationOperator(o DataValidationOperator) string { // settings, show error alert after invalid data is entered with "Stop" style // and custom title "error body": // -// dvRange := excelize.NewDataValidation(true) -// dvRange.Sqref = "A1:B2" -// dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorBetween) -// dvRange.SetError(excelize.DataValidationErrorStyleStop, "error title", "error body") -// err := f.AddDataValidation("Sheet1", dvRange) +// dvRange := excelize.NewDataValidation(true) +// dvRange.Sqref = "A1:B2" +// dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorBetween) +// dvRange.SetError(excelize.DataValidationErrorStyleStop, "error title", "error body") +// err := f.AddDataValidation("Sheet1", dvRange) // // Example 2, set data validation on Sheet1!A3:B4 with validation criteria // settings, and show input message when cell is selected: // -// dvRange = excelize.NewDataValidation(true) -// dvRange.Sqref = "A3:B4" -// dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorGreaterThan) -// dvRange.SetInput("input title", "input body") -// err = f.AddDataValidation("Sheet1", dvRange) +// dvRange = excelize.NewDataValidation(true) +// dvRange.Sqref = "A3:B4" +// dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorGreaterThan) +// dvRange.SetInput("input title", "input body") +// err = f.AddDataValidation("Sheet1", dvRange) // // Example 3, set data validation on Sheet1!A5:B6 with validation criteria // settings, create in-cell dropdown by allowing list source: // -// dvRange = excelize.NewDataValidation(true) -// dvRange.Sqref = "A5:B6" -// dvRange.SetDropList([]string{"1", "2", "3"}) -// err = f.AddDataValidation("Sheet1", dvRange) -// +// dvRange = excelize.NewDataValidation(true) +// dvRange.Sqref = "A5:B6" +// dvRange.SetDropList([]string{"1", "2", "3"}) +// err = f.AddDataValidation("Sheet1", dvRange) func (f *File) AddDataValidation(sheet string, dv *DataValidation) error { ws, err := f.workSheetReader(sheet) if err != nil { diff --git a/docProps.go b/docProps.go index fe6f21447b..df15b57dc8 100644 --- a/docProps.go +++ b/docProps.go @@ -22,50 +22,49 @@ import ( // SetAppProps provides a function to set document application properties. The // properties that can be set are: // -// Property | Description -// -------------------+-------------------------------------------------------------------------- -// Application | The name of the application that created this document. -// | -// ScaleCrop | Indicates the display mode of the document thumbnail. Set this element -// | to 'true' to enable scaling of the document thumbnail to the display. Set -// | this element to 'false' to enable cropping of the document thumbnail to -// | show only sections that will fit the display. -// | -// DocSecurity | Security level of a document as a numeric value. Document security is -// | defined as: -// | 1 - Document is password protected. -// | 2 - Document is recommended to be opened as read-only. -// | 3 - Document is enforced to be opened as read-only. -// | 4 - Document is locked for annotation. -// | -// Company | The name of a company associated with the document. -// | -// LinksUpToDate | Indicates whether hyperlinks in a document are up-to-date. Set this -// | element to 'true' to indicate that hyperlinks are updated. Set this -// | element to 'false' to indicate that hyperlinks are outdated. -// | -// HyperlinksChanged | Specifies that one or more hyperlinks in this part were updated -// | exclusively in this part by a producer. The next producer to open this -// | document shall update the hyperlink relationships with the new -// | hyperlinks specified in this part. -// | -// AppVersion | Specifies the version of the application which produced this document. -// | The content of this element shall be of the form XX.YYYY where X and Y -// | represent numerical values, or the document shall be considered -// | non-conformant. +// Property | Description +// -------------------+-------------------------------------------------------------------------- +// Application | The name of the application that created this document. +// | +// ScaleCrop | Indicates the display mode of the document thumbnail. Set this element +// | to 'true' to enable scaling of the document thumbnail to the display. Set +// | this element to 'false' to enable cropping of the document thumbnail to +// | show only sections that will fit the display. +// | +// DocSecurity | Security level of a document as a numeric value. Document security is +// | defined as: +// | 1 - Document is password protected. +// | 2 - Document is recommended to be opened as read-only. +// | 3 - Document is enforced to be opened as read-only. +// | 4 - Document is locked for annotation. +// | +// Company | The name of a company associated with the document. +// | +// LinksUpToDate | Indicates whether hyperlinks in a document are up-to-date. Set this +// | element to 'true' to indicate that hyperlinks are updated. Set this +// | element to 'false' to indicate that hyperlinks are outdated. +// | +// HyperlinksChanged | Specifies that one or more hyperlinks in this part were updated +// | exclusively in this part by a producer. The next producer to open this +// | document shall update the hyperlink relationships with the new +// | hyperlinks specified in this part. +// | +// AppVersion | Specifies the version of the application which produced this document. +// | The content of this element shall be of the form XX.YYYY where X and Y +// | represent numerical values, or the document shall be considered +// | non-conformant. // // For example: // -// err := f.SetAppProps(&excelize.AppProperties{ -// Application: "Microsoft Excel", -// ScaleCrop: true, -// DocSecurity: 3, -// Company: "Company Name", -// LinksUpToDate: true, -// HyperlinksChanged: true, -// AppVersion: "16.0000", -// }) -// +// err := f.SetAppProps(&excelize.AppProperties{ +// Application: "Microsoft Excel", +// ScaleCrop: true, +// DocSecurity: 3, +// Company: "Company Name", +// LinksUpToDate: true, +// HyperlinksChanged: true, +// AppVersion: "16.0000", +// }) func (f *File) SetAppProps(appProperties *AppProperties) (err error) { var ( app *xlsxProperties @@ -122,54 +121,53 @@ func (f *File) GetAppProps() (ret *AppProperties, err error) { // SetDocProps provides a function to set document core properties. The // properties that can be set are: // -// Property | Description -// ----------------+----------------------------------------------------------------------------- -// Title | The name given to the resource. -// | -// Subject | The topic of the content of the resource. -// | -// Creator | An entity primarily responsible for making the content of the resource. -// | -// Keywords | A delimited set of keywords to support searching and indexing. This is -// | typically a list of terms that are not available elsewhere in the properties. -// | -// Description | An explanation of the content of the resource. -// | -// LastModifiedBy | The user who performed the last modification. The identification is -// | environment-specific. -// | -// Language | The language of the intellectual content of the resource. -// | -// Identifier | An unambiguous reference to the resource within a given context. -// | -// Revision | The topic of the content of the resource. -// | -// ContentStatus | The status of the content. For example: Values might include "Draft", -// | "Reviewed" and "Final" -// | -// Category | A categorization of the content of this package. -// | -// Version | The version number. This value is set by the user or by the application. +// Property | Description +// ----------------+----------------------------------------------------------------------------- +// Title | The name given to the resource. +// | +// Subject | The topic of the content of the resource. +// | +// Creator | An entity primarily responsible for making the content of the resource. +// | +// Keywords | A delimited set of keywords to support searching and indexing. This is +// | typically a list of terms that are not available elsewhere in the properties. +// | +// Description | An explanation of the content of the resource. +// | +// LastModifiedBy | The user who performed the last modification. The identification is +// | environment-specific. +// | +// Language | The language of the intellectual content of the resource. +// | +// Identifier | An unambiguous reference to the resource within a given context. +// | +// Revision | The topic of the content of the resource. +// | +// ContentStatus | The status of the content. For example: Values might include "Draft", +// | "Reviewed" and "Final" +// | +// Category | A categorization of the content of this package. +// | +// Version | The version number. This value is set by the user or by the application. // // For example: // -// err := f.SetDocProps(&excelize.DocProperties{ -// Category: "category", -// ContentStatus: "Draft", -// Created: "2019-06-04T22:00:10Z", -// Creator: "Go Excelize", -// Description: "This file created by Go Excelize", -// Identifier: "xlsx", -// Keywords: "Spreadsheet", -// LastModifiedBy: "Go Author", -// Modified: "2019-06-04T22:00:10Z", -// Revision: "0", -// Subject: "Test Subject", -// Title: "Test Title", -// Language: "en-US", -// Version: "1.0.0", -// }) -// +// err := f.SetDocProps(&excelize.DocProperties{ +// Category: "category", +// ContentStatus: "Draft", +// Created: "2019-06-04T22:00:10Z", +// Creator: "Go Excelize", +// Description: "This file created by Go Excelize", +// Identifier: "xlsx", +// Keywords: "Spreadsheet", +// LastModifiedBy: "Go Author", +// Modified: "2019-06-04T22:00:10Z", +// Revision: "0", +// Subject: "Test Subject", +// Title: "Test Title", +// Language: "en-US", +// Version: "1.0.0", +// }) func (f *File) SetDocProps(docProperties *DocProperties) (err error) { var ( core *decodeCoreProperties diff --git a/excelize.go b/excelize.go index 6603db097d..ef438dd8dd 100644 --- a/excelize.go +++ b/excelize.go @@ -92,10 +92,10 @@ type Options struct { // spreadsheet file struct for it. For example, open spreadsheet with // password protection: // -// f, err := excelize.OpenFile("Book1.xlsx", excelize.Options{Password: "password"}) -// if err != nil { -// return -// } +// f, err := excelize.OpenFile("Book1.xlsx", excelize.Options{Password: "password"}) +// if err != nil { +// return +// } // // Close the file by Close function after opening the spreadsheet. func OpenFile(filename string, opt ...Options) (*File, error) { @@ -403,21 +403,20 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { // // For example: // -// -// -// SUM(Sheet2!D2,Sheet2!D11) -// 100 -// -// +// +// +// SUM(Sheet2!D2,Sheet2!D11) +// 100 +// +// // // to // -// -// -// SUM(Sheet2!D2,Sheet2!D11) -// -// -// +// +// +// SUM(Sheet2!D2,Sheet2!D11) +// +// func (f *File) UpdateLinkedValue() error { wb := f.workbookReader() // recalculate formulas @@ -445,16 +444,15 @@ func (f *File) UpdateLinkedValue() error { // AddVBAProject provides the method to add vbaProject.bin file which contains // functions and/or macros. The file extension should be .xlsm. For example: // -// if err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")); err != nil { -// fmt.Println(err) -// } -// if err := f.AddVBAProject("vbaProject.bin"); err != nil { -// fmt.Println(err) -// } -// if err := f.SaveAs("macros.xlsm"); err != nil { -// fmt.Println(err) -// } -// +// if err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")); err != nil { +// fmt.Println(err) +// } +// if err := f.AddVBAProject("vbaProject.bin"); err != nil { +// fmt.Println(err) +// } +// if err := f.SaveAs("macros.xlsm"); err != nil { +// fmt.Println(err) +// } func (f *File) AddVBAProject(bin string) error { var err error // Check vbaProject.bin exists first. diff --git a/file.go b/file.go index ce8b138336..065e7c5a52 100644 --- a/file.go +++ b/file.go @@ -24,8 +24,7 @@ import ( // NewFile provides a function to create new file by default template. // For example: // -// f := NewFile() -// +// f := NewFile() func NewFile() *File { f := newFile() f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels)) diff --git a/lib.go b/lib.go index 99118ff079..0408139c64 100644 --- a/lib.go +++ b/lib.go @@ -148,8 +148,7 @@ func readFile(file *zip.File) ([]byte, error) { // // Example: // -// excelize.SplitCellName("AK74") // return "AK", 74, nil -// +// excelize.SplitCellName("AK74") // return "AK", 74, nil func SplitCellName(cell string) (string, int, error) { alpha := func(r rune) bool { return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || (r == 36) @@ -192,8 +191,7 @@ func JoinCellName(col string, row int) (string, error) { // // Example: // -// excelize.ColumnNameToNumber("AK") // returns 37, nil -// +// excelize.ColumnNameToNumber("AK") // returns 37, nil func ColumnNameToNumber(name string) (int, error) { if len(name) == 0 { return -1, newInvalidColumnNameError(name) @@ -222,8 +220,7 @@ func ColumnNameToNumber(name string) (int, error) { // // Example: // -// excelize.ColumnNumberToName(37) // returns "AK", nil -// +// excelize.ColumnNumberToName(37) // returns "AK", nil func ColumnNumberToName(num int) (string, error) { if num < MinColumns || num > MaxColumns { return "", ErrColumnNumber @@ -241,9 +238,8 @@ func ColumnNumberToName(num int) (string, error) { // // Example: // -// excelize.CellNameToCoordinates("A1") // returns 1, 1, nil -// excelize.CellNameToCoordinates("Z3") // returns 26, 3, nil -// +// excelize.CellNameToCoordinates("A1") // returns 1, 1, nil +// excelize.CellNameToCoordinates("Z3") // returns 26, 3, nil func CellNameToCoordinates(cell string) (int, int, error) { colName, row, err := SplitCellName(cell) if err != nil { @@ -261,9 +257,8 @@ func CellNameToCoordinates(cell string) (int, int, error) { // // Example: // -// excelize.CoordinatesToCellName(1, 1) // returns "A1", nil -// excelize.CoordinatesToCellName(1, 1, true) // returns "$A$1", nil -// +// excelize.CoordinatesToCellName(1, 1) // returns "A1", nil +// excelize.CoordinatesToCellName(1, 1, true) // returns "$A$1", nil func CoordinatesToCellName(col, row int, abs ...bool) (string, error) { if col < 1 || row < 1 { return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row) @@ -641,7 +636,7 @@ func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte if attr, ok := f.xmlAttr[path]; ok { newXmlns = []byte(genXMLNamespace(attr)) } - return bytesReplace(contentMarshal, oldXmlns, newXmlns, -1) + return bytesReplace(contentMarshal, oldXmlns, bytes.ReplaceAll(newXmlns, []byte(" mc:Ignorable=\"r\""), []byte{}), -1) } // addNameSpaces provides a function to add an XML attribute by the given diff --git a/merge.go b/merge.go index 0f57826e37..d7400a2256 100644 --- a/merge.go +++ b/merge.go @@ -27,25 +27,24 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) { // discards the other values. For example create a merged cell of D3:E9 on // Sheet1: // -// err := f.MergeCell("Sheet1", "D3", "E9") +// err := f.MergeCell("Sheet1", "D3", "E9") // // If you create a merged cell that overlaps with another existing merged cell, // those merged cells that already exist will be removed. The cell coordinates // tuple after merging in the following range will be: A1(x3,y1) D1(x2,y1) // A8(x3,y4) D8(x2,y4) // -// B1(x1,y1) D1(x2,y1) -// +------------------------+ -// | | -// A4(x3,y3) | C4(x4,y3) | -// +------------------------+ | -// | | | | -// | |B5(x1,y2) | D5(x2,y2)| -// | +------------------------+ -// | | -// |A8(x3,y4) C8(x4,y4)| -// +------------------------+ -// +// B1(x1,y1) D1(x2,y1) +// +------------------------+ +// | | +// A4(x3,y3) | C4(x4,y3) | +// +------------------------+ | +// | | | | +// | |B5(x1,y2) | D5(x2,y2)| +// | +------------------------+ +// | | +// |A8(x3,y4) C8(x4,y4)| +// +------------------------+ func (f *File) MergeCell(sheet, hCell, vCell string) error { rect, err := areaRefToCoordinates(hCell + ":" + vCell) if err != nil { @@ -74,7 +73,7 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error { // UnmergeCell provides a function to unmerge a given coordinate area. // For example unmerge area D3:E9 on Sheet1: // -// err := f.UnmergeCell("Sheet1", "D3", "E9") +// err := f.UnmergeCell("Sheet1", "D3", "E9") // // Attention: overlapped areas will also be unmerged. func (f *File) UnmergeCell(sheet string, hCell, vCell string) error { diff --git a/picture.go b/picture.go index c3d0df7050..c78df93cf3 100644 --- a/picture.go +++ b/picture.go @@ -42,34 +42,34 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // format set (such as offset, scale, aspect ratio setting and print settings) // and file path. For example: // -// package main +// package main // -// import ( -// _ "image/gif" -// _ "image/jpeg" -// _ "image/png" +// import ( +// _ "image/gif" +// _ "image/jpeg" +// _ "image/png" // -// "github.com/xuri/excelize/v2" -// ) +// "github.com/xuri/excelize/v2" +// ) // -// func main() { -// f := excelize.NewFile() -// // Insert a picture. -// if err := f.AddPicture("Sheet1", "A2", "image.jpg", ""); err != nil { -// fmt.Println(err) -// } -// // Insert a picture scaling in the cell with location hyperlink. -// if err := f.AddPicture("Sheet1", "D2", "image.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`); err != nil { -// fmt.Println(err) -// } -// // Insert a picture offset in the cell with external hyperlink, printing and positioning support. -// if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`); err != nil { -// fmt.Println(err) -// } -// if err := f.SaveAs("Book1.xlsx"); err != nil { -// fmt.Println(err) -// } -// } +// func main() { +// f := excelize.NewFile() +// // Insert a picture. +// if err := f.AddPicture("Sheet1", "A2", "image.jpg", ""); err != nil { +// fmt.Println(err) +// } +// // Insert a picture scaling in the cell with location hyperlink. +// if err := f.AddPicture("Sheet1", "D2", "image.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`); err != nil { +// fmt.Println(err) +// } +// // Insert a picture offset in the cell with external hyperlink, printing and positioning support. +// if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`); err != nil { +// fmt.Println(err) +// } +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// fmt.Println(err) +// } +// } // // The optional parameter "autofit" specifies if you make image size auto-fits the // cell, the default value of that is 'false'. @@ -106,7 +106,6 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // // The optional parameter "y_scale" specifies the vertical scale of images, // the default value of that is 1.0 which presents 100%. -// func (f *File) AddPicture(sheet, cell, picture, format string) error { var err error // Check picture exists first. @@ -126,31 +125,30 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // picture format set (such as offset, scale, aspect ratio setting and print // settings), file base name, extension name and file bytes. For example: // -// package main -// -// import ( -// "fmt" -// _ "image/jpeg" -// "io/ioutil" +// package main // -// "github.com/xuri/excelize/v2" -// ) +// import ( +// "fmt" +// _ "image/jpeg" +// "io/ioutil" // -// func main() { -// f := excelize.NewFile() +// "github.com/xuri/excelize/v2" +// ) // -// file, err := ioutil.ReadFile("image.jpg") -// if err != nil { -// fmt.Println(err) -// } -// if err := f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file); err != nil { -// fmt.Println(err) -// } -// if err := f.SaveAs("Book1.xlsx"); err != nil { -// fmt.Println(err) -// } -// } +// func main() { +// f := excelize.NewFile() // +// file, err := ioutil.ReadFile("image.jpg") +// if err != nil { +// fmt.Println(err) +// } +// if err := f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file); err != nil { +// fmt.Println(err) +// } +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// fmt.Println(err) +// } +// } func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, file []byte) error { var drawingHyperlinkRID int var hyperlinkType string @@ -474,25 +472,24 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // returns the file name in spreadsheet and file contents as []byte data // types. For example: // -// f, err := excelize.OpenFile("Book1.xlsx") -// if err != nil { -// fmt.Println(err) -// return -// } -// defer func() { -// if err := f.Close(); err != nil { -// fmt.Println(err) -// } -// }() -// file, raw, err := f.GetPicture("Sheet1", "A2") -// if err != nil { -// fmt.Println(err) -// return -// } -// if err := ioutil.WriteFile(file, raw, 0644); err != nil { -// fmt.Println(err) -// } -// +// f, err := excelize.OpenFile("Book1.xlsx") +// if err != nil { +// fmt.Println(err) +// return +// } +// defer func() { +// if err := f.Close(); err != nil { +// fmt.Println(err) +// } +// }() +// file, raw, err := f.GetPicture("Sheet1", "A2") +// if err != nil { +// fmt.Println(err) +// return +// } +// if err := ioutil.WriteFile(file, raw, 0644); err != nil { +// fmt.Println(err) +// } func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { col, row, err := CellNameToCoordinates(cell) if err != nil { diff --git a/pivotTable.go b/pivotTable.go index 73e5d34cfa..bd9fee62ce 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -22,10 +22,9 @@ import ( // // PivotTableStyleName: The built-in pivot table style names // -// PivotStyleLight1 - PivotStyleLight28 -// PivotStyleMedium1 - PivotStyleMedium28 -// PivotStyleDark1 - PivotStyleDark28 -// +// PivotStyleLight1 - PivotStyleLight28 +// PivotStyleMedium1 - PivotStyleMedium28 +// PivotStyleDark1 - PivotStyleDark28 type PivotTableOption struct { pivotTableSheetName string DataRange string @@ -55,17 +54,17 @@ type PivotTableOption struct { // field. The default value is sum. The possible values for this attribute // are: // -// Average -// Count -// CountNums -// Max -// Min -// Product -// StdDev -// StdDevp -// Sum -// Var -// Varp +// Average +// Count +// CountNums +// Max +// Min +// Product +// StdDev +// StdDevp +// Sum +// Var +// Varp // // Name specifies the name of the data field. Maximum 255 characters // are allowed in data field name, excess characters will be truncated. @@ -85,51 +84,50 @@ type PivotTableField struct { // For example, create a pivot table on the Sheet1!$G$2:$M$34 area with the // region Sheet1!$A$1:$E$31 as the data source, summarize by sum for sales: // -// package main -// -// import ( -// "fmt" -// "math/rand" +// package main // -// "github.com/xuri/excelize/v2" -// ) +// import ( +// "fmt" +// "math/rand" // -// func main() { -// f := excelize.NewFile() -// // Create some data in a sheet -// month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} -// year := []int{2017, 2018, 2019} -// types := []string{"Meat", "Dairy", "Beverages", "Produce"} -// region := []string{"East", "West", "North", "South"} -// f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}) -// for row := 2; row < 32; row++ { -// f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)]) -// f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), year[rand.Intn(3)]) -// f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)]) -// f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000)) -// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]) -// } -// if err := f.AddPivotTable(&excelize.PivotTableOption{ -// DataRange: "Sheet1!$A$1:$E$31", -// PivotTableRange: "Sheet1!$G$2:$M$34", -// Rows: []excelize.PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, -// Filter: []excelize.PivotTableField{{Data: "Region"}}, -// Columns: []excelize.PivotTableField{{Data: "Type", DefaultSubtotal: true}}, -// Data: []excelize.PivotTableField{{Data: "Sales", Name: "Summarize", Subtotal: "Sum"}}, -// RowGrandTotals: true, -// ColGrandTotals: true, -// ShowDrill: true, -// ShowRowHeaders: true, -// ShowColHeaders: true, -// ShowLastColumn: true, -// }); err != nil { -// fmt.Println(err) -// } -// if err := f.SaveAs("Book1.xlsx"); err != nil { -// fmt.Println(err) -// } -// } +// "github.com/xuri/excelize/v2" +// ) // +// func main() { +// f := excelize.NewFile() +// // Create some data in a sheet +// month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} +// year := []int{2017, 2018, 2019} +// types := []string{"Meat", "Dairy", "Beverages", "Produce"} +// region := []string{"East", "West", "North", "South"} +// f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}) +// for row := 2; row < 32; row++ { +// f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), year[rand.Intn(3)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000)) +// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]) +// } +// if err := f.AddPivotTable(&excelize.PivotTableOption{ +// DataRange: "Sheet1!$A$1:$E$31", +// PivotTableRange: "Sheet1!$G$2:$M$34", +// Rows: []excelize.PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, +// Filter: []excelize.PivotTableField{{Data: "Region"}}, +// Columns: []excelize.PivotTableField{{Data: "Type", DefaultSubtotal: true}}, +// Data: []excelize.PivotTableField{{Data: "Sales", Name: "Summarize", Subtotal: "Sum"}}, +// RowGrandTotals: true, +// ColGrandTotals: true, +// ShowDrill: true, +// ShowRowHeaders: true, +// ShowColHeaders: true, +// ShowLastColumn: true, +// }); err != nil { +// fmt.Println(err) +// } +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// fmt.Println(err) +// } +// } func (f *File) AddPivotTable(opt *PivotTableOption) error { // parameter validation _, pivotTableSheetPath, err := f.parseFormatPivotTableSet(opt) diff --git a/rows.go b/rows.go index 457f59b729..9eef6286b0 100644 --- a/rows.go +++ b/rows.go @@ -37,18 +37,17 @@ import ( // For example, get and traverse the value of all cells by rows on a worksheet // named 'Sheet1': // -// rows, err := f.GetRows("Sheet1") -// if err != nil { -// fmt.Println(err) -// return -// } -// for _, row := range rows { -// for _, colCell := range row { -// fmt.Print(colCell, "\t") -// } -// fmt.Println() -// } -// +// rows, err := f.GetRows("Sheet1") +// if err != nil { +// fmt.Println(err) +// return +// } +// for _, row := range rows { +// for _, colCell := range row { +// fmt.Print(colCell, "\t") +// } +// fmt.Println() +// } func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) { rows, err := f.Rows(sheet) if err != nil { @@ -240,25 +239,24 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta // Rows returns a rows iterator, used for streaming reading data for a // worksheet with a large data. For example: // -// rows, err := f.Rows("Sheet1") -// if err != nil { -// fmt.Println(err) -// return -// } -// for rows.Next() { -// row, err := rows.Columns() -// if err != nil { -// fmt.Println(err) -// } -// for _, colCell := range row { -// fmt.Print(colCell, "\t") -// } -// fmt.Println() -// } -// if err = rows.Close(); err != nil { -// fmt.Println(err) -// } -// +// rows, err := f.Rows("Sheet1") +// if err != nil { +// fmt.Println(err) +// return +// } +// for rows.Next() { +// row, err := rows.Columns() +// if err != nil { +// fmt.Println(err) +// } +// for _, colCell := range row { +// fmt.Print(colCell, "\t") +// } +// fmt.Println() +// } +// if err = rows.Close(); err != nil { +// fmt.Println(err) +// } func (f *File) Rows(sheet string) (*Rows, error) { name, ok := f.getSheetXMLPath(sheet) if !ok { @@ -344,8 +342,7 @@ func (f *File) xmlDecoder(name string) (bool, *xml.Decoder, *os.File, error) { // SetRowHeight provides a function to set the height of a single row. For // example, set the height of the first row in Sheet1: // -// err := f.SetRowHeight("Sheet1", 1, 50) -// +// err := f.SetRowHeight("Sheet1", 1, 50) func (f *File) SetRowHeight(sheet string, row int, height float64) error { if row < 1 { return newInvalidRowNumberError(row) @@ -385,8 +382,7 @@ func (f *File) getRowHeight(sheet string, row int) int { // GetRowHeight provides a function to get row height by given worksheet name // and row number. For example, get the height of the first row in Sheet1: // -// height, err := f.GetRowHeight("Sheet1", 1) -// +// height, err := f.GetRowHeight("Sheet1", 1) func (f *File) GetRowHeight(sheet string, row int) (float64, error) { if row < 1 { return defaultRowHeightPixels, newInvalidRowNumberError(row) @@ -517,8 +513,7 @@ func roundPrecision(text string, prec int) string { // SetRowVisible provides a function to set visible of a single row by given // worksheet name and Excel row number. For example, hide row 2 in Sheet1: // -// err := f.SetRowVisible("Sheet1", 2, false) -// +// err := f.SetRowVisible("Sheet1", 2, false) func (f *File) SetRowVisible(sheet string, row int, visible bool) error { if row < 1 { return newInvalidRowNumberError(row) @@ -537,8 +532,7 @@ func (f *File) SetRowVisible(sheet string, row int, visible bool) error { // worksheet name and Excel row number. For example, get visible state of row // 2 in Sheet1: // -// visible, err := f.GetRowVisible("Sheet1", 2) -// +// visible, err := f.GetRowVisible("Sheet1", 2) func (f *File) GetRowVisible(sheet string, row int) (bool, error) { if row < 1 { return false, newInvalidRowNumberError(row) @@ -558,8 +552,7 @@ func (f *File) GetRowVisible(sheet string, row int) (bool, error) { // single row by given worksheet name and Excel row number. The value of // parameter 'level' is 1-7. For example, outline row 2 in Sheet1 to level 1: // -// err := f.SetRowOutlineLevel("Sheet1", 2, 1) -// +// err := f.SetRowOutlineLevel("Sheet1", 2, 1) func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error { if row < 1 { return newInvalidRowNumberError(row) @@ -580,8 +573,7 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error { // single row by given worksheet name and Excel row number. For example, get // outline number of row 2 in Sheet1: // -// level, err := f.GetRowOutlineLevel("Sheet1", 2) -// +// level, err := f.GetRowOutlineLevel("Sheet1", 2) func (f *File) GetRowOutlineLevel(sheet string, row int) (uint8, error) { if row < 1 { return 0, newInvalidRowNumberError(row) @@ -599,7 +591,7 @@ func (f *File) GetRowOutlineLevel(sheet string, row int) (uint8, error) { // RemoveRow provides a function to remove single row by given worksheet name // and Excel row number. For example, remove row 3 in Sheet1: // -// err := f.RemoveRow("Sheet1", 3) +// err := f.RemoveRow("Sheet1", 3) // // Use this method with caution, which will affect changes in references such // as formulas, charts, and so on. If there is any referenced value of the @@ -633,7 +625,7 @@ func (f *File) RemoveRow(sheet string, row int) error { // number starting from 1. For example, create a new row before row 3 in // Sheet1: // -// err := f.InsertRow("Sheet1", 3) +// err := f.InsertRow("Sheet1", 3) // // Use this method with caution, which will affect changes in references such // as formulas, charts, and so on. If there is any referenced value of the @@ -648,7 +640,7 @@ func (f *File) InsertRow(sheet string, row int) error { // DuplicateRow inserts a copy of specified row (by its Excel row number) below // -// err := f.DuplicateRow("Sheet1", 2) +// err := f.DuplicateRow("Sheet1", 2) // // Use this method with caution, which will affect changes in references such // as formulas, charts, and so on. If there is any referenced value of the @@ -661,7 +653,7 @@ func (f *File) DuplicateRow(sheet string, row int) error { // DuplicateRowTo inserts a copy of specified row by it Excel number // to specified row position moving down exists rows after target position // -// err := f.DuplicateRowTo("Sheet1", 2, 7) +// err := f.DuplicateRowTo("Sheet1", 2, 7) // // Use this method with caution, which will affect changes in references such // as formulas, charts, and so on. If there is any referenced value of the @@ -758,24 +750,24 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in // checkRow provides a function to check and fill each column element for all // rows and make that is continuous in a worksheet of XML. For example: // -// -// -// -// -// -// +// +// +// +// +// +// // // in this case, we should to change it to // -// -// -// -// -// -// -// -// -// +// +// +// +// +// +// +// +// +// // // Noteice: this method could be very slow for large spreadsheets (more than // 3000 rows one sheet). @@ -843,12 +835,11 @@ func checkRow(ws *xlsxWorksheet) error { // // For example set style of row 1 on Sheet1: // -// err = f.SetRowStyle("Sheet1", 1, 1, styleID) +// err = f.SetRowStyle("Sheet1", 1, 1, styleID) // // Set style of rows 1 to 10 on Sheet1: // -// err = f.SetRowStyle("Sheet1", 1, 10, styleID) -// +// err = f.SetRowStyle("Sheet1", 1, 10, styleID) func (f *File) SetRowStyle(sheet string, start, end, styleID int) error { if end < start { start, end = end, start diff --git a/shape.go b/shape.go index 58751b25b9..4fca348128 100644 --- a/shape.go +++ b/shape.go @@ -39,245 +39,244 @@ func parseFormatShapeSet(formatSet string) (*formatShape, error) { // print settings) and properties set. For example, add text box (rect shape) // in Sheet1: // -// err := f.AddShape("Sheet1", "G6", `{ -// "type": "rect", -// "color": -// { -// "line": "#4286F4", -// "fill": "#8eb9ff" -// }, -// "paragraph": [ -// { -// "text": "Rectangle Shape", -// "font": -// { -// "bold": true, -// "italic": true, -// "family": "Times New Roman", -// "size": 36, -// "color": "#777777", -// "underline": "sng" -// } -// }], -// "width": 180, -// "height": 90, -// "line": -// { -// "width": 1.2 -// } -// }`) +// err := f.AddShape("Sheet1", "G6", `{ +// "type": "rect", +// "color": +// { +// "line": "#4286F4", +// "fill": "#8eb9ff" +// }, +// "paragraph": [ +// { +// "text": "Rectangle Shape", +// "font": +// { +// "bold": true, +// "italic": true, +// "family": "Times New Roman", +// "size": 36, +// "color": "#777777", +// "underline": "sng" +// } +// }], +// "width": 180, +// "height": 90, +// "line": +// { +// "width": 1.2 +// } +// }`) // // The following shows the type of shape supported by excelize: // -// accentBorderCallout1 (Callout 1 with Border and Accent Shape) -// accentBorderCallout2 (Callout 2 with Border and Accent Shape) -// accentBorderCallout3 (Callout 3 with Border and Accent Shape) -// accentCallout1 (Callout 1 Shape) -// accentCallout2 (Callout 2 Shape) -// accentCallout3 (Callout 3 Shape) -// actionButtonBackPrevious (Back or Previous Button Shape) -// actionButtonBeginning (Beginning Button Shape) -// actionButtonBlank (Blank Button Shape) -// actionButtonDocument (Document Button Shape) -// actionButtonEnd (End Button Shape) -// actionButtonForwardNext (Forward or Next Button Shape) -// actionButtonHelp (Help Button Shape) -// actionButtonHome (Home Button Shape) -// actionButtonInformation (Information Button Shape) -// actionButtonMovie (Movie Button Shape) -// actionButtonReturn (Return Button Shape) -// actionButtonSound (Sound Button Shape) -// arc (Curved Arc Shape) -// bentArrow (Bent Arrow Shape) -// bentConnector2 (Bent Connector 2 Shape) -// bentConnector3 (Bent Connector 3 Shape) -// bentConnector4 (Bent Connector 4 Shape) -// bentConnector5 (Bent Connector 5 Shape) -// bentUpArrow (Bent Up Arrow Shape) -// bevel (Bevel Shape) -// blockArc (Block Arc Shape) -// borderCallout1 (Callout 1 with Border Shape) -// borderCallout2 (Callout 2 with Border Shape) -// borderCallout3 (Callout 3 with Border Shape) -// bracePair (Brace Pair Shape) -// bracketPair (Bracket Pair Shape) -// callout1 (Callout 1 Shape) -// callout2 (Callout 2 Shape) -// callout3 (Callout 3 Shape) -// can (Can Shape) -// chartPlus (Chart Plus Shape) -// chartStar (Chart Star Shape) -// chartX (Chart X Shape) -// chevron (Chevron Shape) -// chord (Chord Shape) -// circularArrow (Circular Arrow Shape) -// cloud (Cloud Shape) -// cloudCallout (Callout Cloud Shape) -// corner (Corner Shape) -// cornerTabs (Corner Tabs Shape) -// cube (Cube Shape) -// curvedConnector2 (Curved Connector 2 Shape) -// curvedConnector3 (Curved Connector 3 Shape) -// curvedConnector4 (Curved Connector 4 Shape) -// curvedConnector5 (Curved Connector 5 Shape) -// curvedDownArrow (Curved Down Arrow Shape) -// curvedLeftArrow (Curved Left Arrow Shape) -// curvedRightArrow (Curved Right Arrow Shape) -// curvedUpArrow (Curved Up Arrow Shape) -// decagon (Decagon Shape) -// diagStripe (Diagonal Stripe Shape) -// diamond (Diamond Shape) -// dodecagon (Dodecagon Shape) -// donut (Donut Shape) -// doubleWave (Double Wave Shape) -// downArrow (Down Arrow Shape) -// downArrowCallout (Callout Down Arrow Shape) -// ellipse (Ellipse Shape) -// ellipseRibbon (Ellipse Ribbon Shape) -// ellipseRibbon2 (Ellipse Ribbon 2 Shape) -// flowChartAlternateProcess (Alternate Process Flow Shape) -// flowChartCollate (Collate Flow Shape) -// flowChartConnector (Connector Flow Shape) -// flowChartDecision (Decision Flow Shape) -// flowChartDelay (Delay Flow Shape) -// flowChartDisplay (Display Flow Shape) -// flowChartDocument (Document Flow Shape) -// flowChartExtract (Extract Flow Shape) -// flowChartInputOutput (Input Output Flow Shape) -// flowChartInternalStorage (Internal Storage Flow Shape) -// flowChartMagneticDisk (Magnetic Disk Flow Shape) -// flowChartMagneticDrum (Magnetic Drum Flow Shape) -// flowChartMagneticTape (Magnetic Tape Flow Shape) -// flowChartManualInput (Manual Input Flow Shape) -// flowChartManualOperation (Manual Operation Flow Shape) -// flowChartMerge (Merge Flow Shape) -// flowChartMultidocument (Multi-Document Flow Shape) -// flowChartOfflineStorage (Offline Storage Flow Shape) -// flowChartOffpageConnector (Off-Page Connector Flow Shape) -// flowChartOnlineStorage (Online Storage Flow Shape) -// flowChartOr (Or Flow Shape) -// flowChartPredefinedProcess (Predefined Process Flow Shape) -// flowChartPreparation (Preparation Flow Shape) -// flowChartProcess (Process Flow Shape) -// flowChartPunchedCard (Punched Card Flow Shape) -// flowChartPunchedTape (Punched Tape Flow Shape) -// flowChartSort (Sort Flow Shape) -// flowChartSummingJunction (Summing Junction Flow Shape) -// flowChartTerminator (Terminator Flow Shape) -// foldedCorner (Folded Corner Shape) -// frame (Frame Shape) -// funnel (Funnel Shape) -// gear6 (Gear 6 Shape) -// gear9 (Gear 9 Shape) -// halfFrame (Half Frame Shape) -// heart (Heart Shape) -// heptagon (Heptagon Shape) -// hexagon (Hexagon Shape) -// homePlate (Home Plate Shape) -// horizontalScroll (Horizontal Scroll Shape) -// irregularSeal1 (Irregular Seal 1 Shape) -// irregularSeal2 (Irregular Seal 2 Shape) -// leftArrow (Left Arrow Shape) -// leftArrowCallout (Callout Left Arrow Shape) -// leftBrace (Left Brace Shape) -// leftBracket (Left Bracket Shape) -// leftCircularArrow (Left Circular Arrow Shape) -// leftRightArrow (Left Right Arrow Shape) -// leftRightArrowCallout (Callout Left Right Arrow Shape) -// leftRightCircularArrow (Left Right Circular Arrow Shape) -// leftRightRibbon (Left Right Ribbon Shape) -// leftRightUpArrow (Left Right Up Arrow Shape) -// leftUpArrow (Left Up Arrow Shape) -// lightningBolt (Lightning Bolt Shape) -// line (Line Shape) -// lineInv (Line Inverse Shape) -// mathDivide (Divide Math Shape) -// mathEqual (Equal Math Shape) -// mathMinus (Minus Math Shape) -// mathMultiply (Multiply Math Shape) -// mathNotEqual (Not Equal Math Shape) -// mathPlus (Plus Math Shape) -// moon (Moon Shape) -// nonIsoscelesTrapezoid (Non-Isosceles Trapezoid Shape) -// noSmoking (No Smoking Shape) -// notchedRightArrow (Notched Right Arrow Shape) -// octagon (Octagon Shape) -// parallelogram (Parallelogram Shape) -// pentagon (Pentagon Shape) -// pie (Pie Shape) -// pieWedge (Pie Wedge Shape) -// plaque (Plaque Shape) -// plaqueTabs (Plaque Tabs Shape) -// plus (Plus Shape) -// quadArrow (Quad-Arrow Shape) -// quadArrowCallout (Callout Quad-Arrow Shape) -// rect (Rectangle Shape) -// ribbon (Ribbon Shape) -// ribbon2 (Ribbon 2 Shape) -// rightArrow (Right Arrow Shape) -// rightArrowCallout (Callout Right Arrow Shape) -// rightBrace (Right Brace Shape) -// rightBracket (Right Bracket Shape) -// round1Rect (One Round Corner Rectangle Shape) -// round2DiagRect (Two Diagonal Round Corner Rectangle Shape) -// round2SameRect (Two Same-side Round Corner Rectangle Shape) -// roundRect (Round Corner Rectangle Shape) -// rtTriangle (Right Triangle Shape) -// smileyFace (Smiley Face Shape) -// snip1Rect (One Snip Corner Rectangle Shape) -// snip2DiagRect (Two Diagonal Snip Corner Rectangle Shape) -// snip2SameRect (Two Same-side Snip Corner Rectangle Shape) -// snipRoundRect (One Snip One Round Corner Rectangle Shape) -// squareTabs (Square Tabs Shape) -// star10 (Ten Pointed Star Shape) -// star12 (Twelve Pointed Star Shape) -// star16 (Sixteen Pointed Star Shape) -// star24 (Twenty Four Pointed Star Shape) -// star32 (Thirty Two Pointed Star Shape) -// star4 (Four Pointed Star Shape) -// star5 (Five Pointed Star Shape) -// star6 (Six Pointed Star Shape) -// star7 (Seven Pointed Star Shape) -// star8 (Eight Pointed Star Shape) -// straightConnector1 (Straight Connector 1 Shape) -// stripedRightArrow (Striped Right Arrow Shape) -// sun (Sun Shape) -// swooshArrow (Swoosh Arrow Shape) -// teardrop (Teardrop Shape) -// trapezoid (Trapezoid Shape) -// triangle (Triangle Shape) -// upArrow (Up Arrow Shape) -// upArrowCallout (Callout Up Arrow Shape) -// upDownArrow (Up Down Arrow Shape) -// upDownArrowCallout (Callout Up Down Arrow Shape) -// uturnArrow (U-Turn Arrow Shape) -// verticalScroll (Vertical Scroll Shape) -// wave (Wave Shape) -// wedgeEllipseCallout (Callout Wedge Ellipse Shape) -// wedgeRectCallout (Callout Wedge Rectangle Shape) -// wedgeRoundRectCallout (Callout Wedge Round Rectangle Shape) +// accentBorderCallout1 (Callout 1 with Border and Accent Shape) +// accentBorderCallout2 (Callout 2 with Border and Accent Shape) +// accentBorderCallout3 (Callout 3 with Border and Accent Shape) +// accentCallout1 (Callout 1 Shape) +// accentCallout2 (Callout 2 Shape) +// accentCallout3 (Callout 3 Shape) +// actionButtonBackPrevious (Back or Previous Button Shape) +// actionButtonBeginning (Beginning Button Shape) +// actionButtonBlank (Blank Button Shape) +// actionButtonDocument (Document Button Shape) +// actionButtonEnd (End Button Shape) +// actionButtonForwardNext (Forward or Next Button Shape) +// actionButtonHelp (Help Button Shape) +// actionButtonHome (Home Button Shape) +// actionButtonInformation (Information Button Shape) +// actionButtonMovie (Movie Button Shape) +// actionButtonReturn (Return Button Shape) +// actionButtonSound (Sound Button Shape) +// arc (Curved Arc Shape) +// bentArrow (Bent Arrow Shape) +// bentConnector2 (Bent Connector 2 Shape) +// bentConnector3 (Bent Connector 3 Shape) +// bentConnector4 (Bent Connector 4 Shape) +// bentConnector5 (Bent Connector 5 Shape) +// bentUpArrow (Bent Up Arrow Shape) +// bevel (Bevel Shape) +// blockArc (Block Arc Shape) +// borderCallout1 (Callout 1 with Border Shape) +// borderCallout2 (Callout 2 with Border Shape) +// borderCallout3 (Callout 3 with Border Shape) +// bracePair (Brace Pair Shape) +// bracketPair (Bracket Pair Shape) +// callout1 (Callout 1 Shape) +// callout2 (Callout 2 Shape) +// callout3 (Callout 3 Shape) +// can (Can Shape) +// chartPlus (Chart Plus Shape) +// chartStar (Chart Star Shape) +// chartX (Chart X Shape) +// chevron (Chevron Shape) +// chord (Chord Shape) +// circularArrow (Circular Arrow Shape) +// cloud (Cloud Shape) +// cloudCallout (Callout Cloud Shape) +// corner (Corner Shape) +// cornerTabs (Corner Tabs Shape) +// cube (Cube Shape) +// curvedConnector2 (Curved Connector 2 Shape) +// curvedConnector3 (Curved Connector 3 Shape) +// curvedConnector4 (Curved Connector 4 Shape) +// curvedConnector5 (Curved Connector 5 Shape) +// curvedDownArrow (Curved Down Arrow Shape) +// curvedLeftArrow (Curved Left Arrow Shape) +// curvedRightArrow (Curved Right Arrow Shape) +// curvedUpArrow (Curved Up Arrow Shape) +// decagon (Decagon Shape) +// diagStripe (Diagonal Stripe Shape) +// diamond (Diamond Shape) +// dodecagon (Dodecagon Shape) +// donut (Donut Shape) +// doubleWave (Double Wave Shape) +// downArrow (Down Arrow Shape) +// downArrowCallout (Callout Down Arrow Shape) +// ellipse (Ellipse Shape) +// ellipseRibbon (Ellipse Ribbon Shape) +// ellipseRibbon2 (Ellipse Ribbon 2 Shape) +// flowChartAlternateProcess (Alternate Process Flow Shape) +// flowChartCollate (Collate Flow Shape) +// flowChartConnector (Connector Flow Shape) +// flowChartDecision (Decision Flow Shape) +// flowChartDelay (Delay Flow Shape) +// flowChartDisplay (Display Flow Shape) +// flowChartDocument (Document Flow Shape) +// flowChartExtract (Extract Flow Shape) +// flowChartInputOutput (Input Output Flow Shape) +// flowChartInternalStorage (Internal Storage Flow Shape) +// flowChartMagneticDisk (Magnetic Disk Flow Shape) +// flowChartMagneticDrum (Magnetic Drum Flow Shape) +// flowChartMagneticTape (Magnetic Tape Flow Shape) +// flowChartManualInput (Manual Input Flow Shape) +// flowChartManualOperation (Manual Operation Flow Shape) +// flowChartMerge (Merge Flow Shape) +// flowChartMultidocument (Multi-Document Flow Shape) +// flowChartOfflineStorage (Offline Storage Flow Shape) +// flowChartOffpageConnector (Off-Page Connector Flow Shape) +// flowChartOnlineStorage (Online Storage Flow Shape) +// flowChartOr (Or Flow Shape) +// flowChartPredefinedProcess (Predefined Process Flow Shape) +// flowChartPreparation (Preparation Flow Shape) +// flowChartProcess (Process Flow Shape) +// flowChartPunchedCard (Punched Card Flow Shape) +// flowChartPunchedTape (Punched Tape Flow Shape) +// flowChartSort (Sort Flow Shape) +// flowChartSummingJunction (Summing Junction Flow Shape) +// flowChartTerminator (Terminator Flow Shape) +// foldedCorner (Folded Corner Shape) +// frame (Frame Shape) +// funnel (Funnel Shape) +// gear6 (Gear 6 Shape) +// gear9 (Gear 9 Shape) +// halfFrame (Half Frame Shape) +// heart (Heart Shape) +// heptagon (Heptagon Shape) +// hexagon (Hexagon Shape) +// homePlate (Home Plate Shape) +// horizontalScroll (Horizontal Scroll Shape) +// irregularSeal1 (Irregular Seal 1 Shape) +// irregularSeal2 (Irregular Seal 2 Shape) +// leftArrow (Left Arrow Shape) +// leftArrowCallout (Callout Left Arrow Shape) +// leftBrace (Left Brace Shape) +// leftBracket (Left Bracket Shape) +// leftCircularArrow (Left Circular Arrow Shape) +// leftRightArrow (Left Right Arrow Shape) +// leftRightArrowCallout (Callout Left Right Arrow Shape) +// leftRightCircularArrow (Left Right Circular Arrow Shape) +// leftRightRibbon (Left Right Ribbon Shape) +// leftRightUpArrow (Left Right Up Arrow Shape) +// leftUpArrow (Left Up Arrow Shape) +// lightningBolt (Lightning Bolt Shape) +// line (Line Shape) +// lineInv (Line Inverse Shape) +// mathDivide (Divide Math Shape) +// mathEqual (Equal Math Shape) +// mathMinus (Minus Math Shape) +// mathMultiply (Multiply Math Shape) +// mathNotEqual (Not Equal Math Shape) +// mathPlus (Plus Math Shape) +// moon (Moon Shape) +// nonIsoscelesTrapezoid (Non-Isosceles Trapezoid Shape) +// noSmoking (No Smoking Shape) +// notchedRightArrow (Notched Right Arrow Shape) +// octagon (Octagon Shape) +// parallelogram (Parallelogram Shape) +// pentagon (Pentagon Shape) +// pie (Pie Shape) +// pieWedge (Pie Wedge Shape) +// plaque (Plaque Shape) +// plaqueTabs (Plaque Tabs Shape) +// plus (Plus Shape) +// quadArrow (Quad-Arrow Shape) +// quadArrowCallout (Callout Quad-Arrow Shape) +// rect (Rectangle Shape) +// ribbon (Ribbon Shape) +// ribbon2 (Ribbon 2 Shape) +// rightArrow (Right Arrow Shape) +// rightArrowCallout (Callout Right Arrow Shape) +// rightBrace (Right Brace Shape) +// rightBracket (Right Bracket Shape) +// round1Rect (One Round Corner Rectangle Shape) +// round2DiagRect (Two Diagonal Round Corner Rectangle Shape) +// round2SameRect (Two Same-side Round Corner Rectangle Shape) +// roundRect (Round Corner Rectangle Shape) +// rtTriangle (Right Triangle Shape) +// smileyFace (Smiley Face Shape) +// snip1Rect (One Snip Corner Rectangle Shape) +// snip2DiagRect (Two Diagonal Snip Corner Rectangle Shape) +// snip2SameRect (Two Same-side Snip Corner Rectangle Shape) +// snipRoundRect (One Snip One Round Corner Rectangle Shape) +// squareTabs (Square Tabs Shape) +// star10 (Ten Pointed Star Shape) +// star12 (Twelve Pointed Star Shape) +// star16 (Sixteen Pointed Star Shape) +// star24 (Twenty Four Pointed Star Shape) +// star32 (Thirty Two Pointed Star Shape) +// star4 (Four Pointed Star Shape) +// star5 (Five Pointed Star Shape) +// star6 (Six Pointed Star Shape) +// star7 (Seven Pointed Star Shape) +// star8 (Eight Pointed Star Shape) +// straightConnector1 (Straight Connector 1 Shape) +// stripedRightArrow (Striped Right Arrow Shape) +// sun (Sun Shape) +// swooshArrow (Swoosh Arrow Shape) +// teardrop (Teardrop Shape) +// trapezoid (Trapezoid Shape) +// triangle (Triangle Shape) +// upArrow (Up Arrow Shape) +// upArrowCallout (Callout Up Arrow Shape) +// upDownArrow (Up Down Arrow Shape) +// upDownArrowCallout (Callout Up Down Arrow Shape) +// uturnArrow (U-Turn Arrow Shape) +// verticalScroll (Vertical Scroll Shape) +// wave (Wave Shape) +// wedgeEllipseCallout (Callout Wedge Ellipse Shape) +// wedgeRectCallout (Callout Wedge Rectangle Shape) +// wedgeRoundRectCallout (Callout Wedge Round Rectangle Shape) // // The following shows the type of text underline supported by excelize: // -// none -// words -// sng -// dbl -// heavy -// dotted -// dottedHeavy -// dash -// dashHeavy -// dashLong -// dashLongHeavy -// dotDash -// dotDashHeavy -// dotDotDash -// dotDotDashHeavy -// wavy -// wavyHeavy -// wavyDbl -// +// none +// words +// sng +// dbl +// heavy +// dotted +// dottedHeavy +// dash +// dashHeavy +// dashLong +// dashLongHeavy +// dotDash +// dotDashHeavy +// dotDotDash +// dotDotDashHeavy +// wavy +// wavyHeavy +// wavyDbl func (f *File) AddShape(sheet, cell, format string) error { formatSet, err := parseFormatShapeSet(format) if err != nil { diff --git a/sheet.go b/sheet.go index 1f2dceaaaa..a62675857a 100644 --- a/sheet.go +++ b/sheet.go @@ -392,19 +392,18 @@ func (f *File) GetSheetIndex(sheet string) int { // GetSheetMap provides a function to get worksheets, chart sheets, dialog // sheets ID and name map of the workbook. For example: // -// f, err := excelize.OpenFile("Book1.xlsx") -// if err != nil { -// return -// } -// defer func() { -// if err := f.Close(); err != nil { -// fmt.Println(err) -// } -// }() -// for index, name := range f.GetSheetMap() { -// fmt.Println(index, name) -// } -// +// f, err := excelize.OpenFile("Book1.xlsx") +// if err != nil { +// return +// } +// defer func() { +// if err := f.Close(); err != nil { +// fmt.Println(err) +// } +// }() +// for index, name := range f.GetSheetMap() { +// fmt.Println(index, name) +// } func (f *File) GetSheetMap() map[int]string { wb := f.workbookReader() sheetMap := map[int]string{} @@ -588,11 +587,10 @@ func (f *File) deleteSheetFromContentTypes(target string) { // target worksheet index. Note that currently doesn't support duplicate // workbooks that contain tables, charts or pictures. For Example: // -// // Sheet1 already exists... -// index := f.NewSheet("Sheet2") -// err := f.CopySheet(1, index) -// return err -// +// // Sheet1 already exists... +// index := f.NewSheet("Sheet2") +// err := f.CopySheet(1, index) +// return err func (f *File) CopySheet(from, to int) error { if from < 0 || to < 0 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" { return ErrSheetIdx @@ -634,14 +632,13 @@ func (f *File) copySheet(from, to int) error { // worksheet has been activated, this setting will be invalidated. Sheet state // values as defined by https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetstatevalues // -// visible -// hidden -// veryHidden +// visible +// hidden +// veryHidden // // For example, hide Sheet1: // -// err := f.SetSheetVisible("Sheet1", false) -// +// err := f.SetSheetVisible("Sheet1", false) func (f *File) SetSheetVisible(sheet string, visible bool) error { sheet = trimSheetName(sheet) content := f.workbookReader() @@ -688,50 +685,50 @@ func parseFormatPanesSet(formatSet string) (*formatPanes, error) { // activePane defines the pane that is active. The possible values for this // attribute are defined in the following table: // -// Enumeration Value | Description -// --------------------------------+------------------------------------------------------------- -// bottomLeft (Bottom Left Pane) | Bottom left pane, when both vertical and horizontal -// | splits are applied. -// | -// | This value is also used when only a horizontal split has -// | been applied, dividing the pane into upper and lower -// | regions. In that case, this value specifies the bottom -// | pane. -// | -// bottomRight (Bottom Right Pane) | Bottom right pane, when both vertical and horizontal -// | splits are applied. -// | -// topLeft (Top Left Pane) | Top left pane, when both vertical and horizontal splits -// | are applied. -// | -// | This value is also used when only a horizontal split has -// | been applied, dividing the pane into upper and lower -// | regions. In that case, this value specifies the top pane. -// | -// | This value is also used when only a vertical split has -// | been applied, dividing the pane into right and left -// | regions. In that case, this value specifies the left pane -// | -// topRight (Top Right Pane) | Top right pane, when both vertical and horizontal -// | splits are applied. -// | -// | This value is also used when only a vertical split has -// | been applied, dividing the pane into right and left -// | regions. In that case, this value specifies the right -// | pane. +// Enumeration Value | Description +// --------------------------------+------------------------------------------------------------- +// bottomLeft (Bottom Left Pane) | Bottom left pane, when both vertical and horizontal +// | splits are applied. +// | +// | This value is also used when only a horizontal split has +// | been applied, dividing the pane into upper and lower +// | regions. In that case, this value specifies the bottom +// | pane. +// | +// bottomRight (Bottom Right Pane) | Bottom right pane, when both vertical and horizontal +// | splits are applied. +// | +// topLeft (Top Left Pane) | Top left pane, when both vertical and horizontal splits +// | are applied. +// | +// | This value is also used when only a horizontal split has +// | been applied, dividing the pane into upper and lower +// | regions. In that case, this value specifies the top pane. +// | +// | This value is also used when only a vertical split has +// | been applied, dividing the pane into right and left +// | regions. In that case, this value specifies the left pane +// | +// topRight (Top Right Pane) | Top right pane, when both vertical and horizontal +// | splits are applied. +// | +// | This value is also used when only a vertical split has +// | been applied, dividing the pane into right and left +// | regions. In that case, this value specifies the right +// | pane. // // Pane state type is restricted to the values supported currently listed in the following table: // -// Enumeration Value | Description -// --------------------------------+------------------------------------------------------------- -// frozen (Frozen) | Panes are frozen, but were not split being frozen. In -// | this state, when the panes are unfrozen again, a single -// | pane results, with no split. -// | -// | In this state, the split bars are not adjustable. -// | -// split (Split) | Panes are split, but not frozen. In this state, the split -// | bars are adjustable by the user. +// Enumeration Value | Description +// --------------------------------+------------------------------------------------------------- +// frozen (Frozen) | Panes are frozen, but were not split being frozen. In +// | this state, when the panes are unfrozen again, a single +// | pane results, with no split. +// | +// | In this state, the split bars are not adjustable. +// | +// split (Split) | Panes are split, but not frozen. In this state, the split +// | bars are adjustable by the user. // // x_split (Horizontal Split Position): Horizontal position of the split, in // 1/20th of a point; 0 (zero) if none. If the pane is frozen, this value @@ -751,22 +748,21 @@ func parseFormatPanesSet(formatSet string) (*formatPanes, error) { // An example of how to freeze column A in the Sheet1 and set the active cell on // Sheet1!K16: // -// f.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`) +// f.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`) // // An example of how to freeze rows 1 to 9 in the Sheet1 and set the active cell // ranges on Sheet1!A11:XFD11: // -// f.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`) +// f.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`) // // An example of how to create split panes in the Sheet1 and set the active cell // on Sheet1!J60: // -// f.SetPanes("Sheet1", `{"freeze":false,"split":true,"x_split":3270,"y_split":1800,"top_left_cell":"N57","active_pane":"bottomLeft","panes":[{"sqref":"I36","active_cell":"I36"},{"sqref":"G33","active_cell":"G33","pane":"topRight"},{"sqref":"J60","active_cell":"J60","pane":"bottomLeft"},{"sqref":"O60","active_cell":"O60","pane":"bottomRight"}]}`) +// f.SetPanes("Sheet1", `{"freeze":false,"split":true,"x_split":3270,"y_split":1800,"top_left_cell":"N57","active_pane":"bottomLeft","panes":[{"sqref":"I36","active_cell":"I36"},{"sqref":"G33","active_cell":"G33","pane":"topRight"},{"sqref":"J60","active_cell":"J60","pane":"bottomLeft"},{"sqref":"O60","active_cell":"O60","pane":"bottomRight"}]}`) // // An example of how to unfreeze and remove all panes on Sheet1: // -// f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) -// +// f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) func (f *File) SetPanes(sheet, panes string) error { fs, _ := parseFormatPanesSet(panes) ws, err := f.workSheetReader(sheet) @@ -806,8 +802,7 @@ func (f *File) SetPanes(sheet, panes string) error { // GetSheetVisible provides a function to get worksheet visible by given worksheet // name. For example, get visible state of Sheet1: // -// f.GetSheetVisible("Sheet1") -// +// f.GetSheetVisible("Sheet1") func (f *File) GetSheetVisible(sheet string) bool { content, name, visible := f.workbookReader(), trimSheetName(sheet), false for k, v := range content.Sheets.Sheet { @@ -828,13 +823,12 @@ func (f *File) GetSheetVisible(sheet string) bool { // // An example of search the coordinates of the value of "100" on Sheet1: // -// result, err := f.SearchSheet("Sheet1", "100") +// result, err := f.SearchSheet("Sheet1", "100") // // An example of search the coordinates where the numerical value in the range // of "0-9" of Sheet1 is described: // -// result, err := f.SearchSheet("Sheet1", "[0-9]", true) -// +// result, err := f.SearchSheet("Sheet1", "[0-9]", true) func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { var ( regSearch bool @@ -961,95 +955,95 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { // // Headers and footers are specified using the following settings fields: // -// Fields | Description -// ------------------+----------------------------------------------------------- -// AlignWithMargins | Align header footer margins with page margins -// DifferentFirst | Different first-page header and footer indicator -// DifferentOddEven | Different odd and even page headers and footers indicator -// ScaleWithDoc | Scale header and footer with document scaling -// OddFooter | Odd Page Footer -// OddHeader | Odd Header -// EvenFooter | Even Page Footer -// EvenHeader | Even Page Header -// FirstFooter | First Page Footer -// FirstHeader | First Page Header +// Fields | Description +// ------------------+----------------------------------------------------------- +// AlignWithMargins | Align header footer margins with page margins +// DifferentFirst | Different first-page header and footer indicator +// DifferentOddEven | Different odd and even page headers and footers indicator +// ScaleWithDoc | Scale header and footer with document scaling +// OddFooter | Odd Page Footer +// OddHeader | Odd Header +// EvenFooter | Even Page Footer +// EvenHeader | Even Page Header +// FirstFooter | First Page Footer +// FirstHeader | First Page Header // // The following formatting codes can be used in 6 string type fields: // OddHeader, OddFooter, EvenHeader, EvenFooter, FirstFooter, FirstHeader // -// Formatting Code | Description -// ------------------------+------------------------------------------------------------------------- -// && | The character "&" -// | -// &font-size | Size of the text font, where font-size is a decimal font size in points -// | -// &"font name,font type" | A text font-name string, font name, and a text font-type string, -// | font type -// | -// &"-,Regular" | Regular text format. Toggles bold and italic modes to off -// | -// &A | Current worksheet's tab name -// | -// &B or &"-,Bold" | Bold text format, from off to on, or vice versa. The default mode is off -// | -// &D | Current date -// | -// &C | Center section -// | -// &E | Double-underline text format -// | -// &F | Current workbook's file name -// | -// &G | Drawing object as background -// | -// &H | Shadow text format -// | -// &I or &"-,Italic" | Italic text format -// | -// &K | Text font color -// | -// | An RGB Color is specified as RRGGBB -// | -// | A Theme Color is specified as TTSNNN where TT is the theme color Id, -// | S is either "+" or "-" of the tint/shade value, and NNN is the -// | tint/shade value -// | -// &L | Left section -// | -// &N | Total number of pages -// | -// &O | Outline text format -// | -// &P[[+|-]n] | Without the optional suffix, the current page number in decimal -// | -// &R | Right section -// | -// &S | Strikethrough text format -// | -// &T | Current time -// | -// &U | Single-underline text format. If double-underline mode is on, the next -// | occurrence in a section specifier toggles double-underline mode to off; -// | otherwise, it toggles single-underline mode, from off to on, or vice -// | versa. The default mode is off -// | -// &X | Superscript text format -// | -// &Y | Subscript text format -// | -// &Z | Current workbook's file path +// Formatting Code | Description +// ------------------------+------------------------------------------------------------------------- +// && | The character "&" +// | +// &font-size | Size of the text font, where font-size is a decimal font size in points +// | +// &"font name,font type" | A text font-name string, font name, and a text font-type string, +// | font type +// | +// &"-,Regular" | Regular text format. Toggles bold and italic modes to off +// | +// &A | Current worksheet's tab name +// | +// &B or &"-,Bold" | Bold text format, from off to on, or vice versa. The default mode is off +// | +// &D | Current date +// | +// &C | Center section +// | +// &E | Double-underline text format +// | +// &F | Current workbook's file name +// | +// &G | Drawing object as background +// | +// &H | Shadow text format +// | +// &I or &"-,Italic" | Italic text format +// | +// &K | Text font color +// | +// | An RGB Color is specified as RRGGBB +// | +// | A Theme Color is specified as TTSNNN where TT is the theme color Id, +// | S is either "+" or "-" of the tint/shade value, and NNN is the +// | tint/shade value +// | +// &L | Left section +// | +// &N | Total number of pages +// | +// &O | Outline text format +// | +// &P[[+|-]n] | Without the optional suffix, the current page number in decimal +// | +// &R | Right section +// | +// &S | Strikethrough text format +// | +// &T | Current time +// | +// &U | Single-underline text format. If double-underline mode is on, the next +// | occurrence in a section specifier toggles double-underline mode to off; +// | otherwise, it toggles single-underline mode, from off to on, or vice +// | versa. The default mode is off +// | +// &X | Superscript text format +// | +// &Y | Subscript text format +// | +// &Z | Current workbook's file path // // For example: // -// err := f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{ -// DifferentFirst: true, -// DifferentOddEven: true, -// OddHeader: "&R&P", -// OddFooter: "&C&F", -// EvenHeader: "&L&P", -// EvenFooter: "&L&D&R&T", -// FirstHeader: `&CCenter &"-,Bold"Bold&"-,Regular"HeaderU+000A&D`, -// }) +// err := f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{ +// DifferentFirst: true, +// DifferentOddEven: true, +// OddHeader: "&R&P", +// OddFooter: "&C&F", +// EvenHeader: "&L&P", +// EvenFooter: "&L&D&R&T", +// FirstHeader: `&CCenter &"-,Bold"Bold&"-,Regular"HeaderU+000A&D`, +// }) // // This example shows: // @@ -1071,7 +1065,6 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { // that same page // // - No footer on the first page -// func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -1112,12 +1105,11 @@ func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error // specified, will be using the XOR algorithm as default. For example, protect // Sheet1 with protection settings: // -// err := f.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ -// AlgorithmName: "SHA-512", -// Password: "password", -// EditScenarios: false, -// }) -// +// err := f.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ +// AlgorithmName: "SHA-512", +// Password: "password", +// EditScenarios: false, +// }) func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -1377,135 +1369,134 @@ func (p *PageLayoutScale) getPageLayout(ps *xlsxPageSetUp) { // // Available options: // -// BlackAndWhite(bool) -// FirstPageNumber(uint) -// PageLayoutOrientation(string) -// PageLayoutPaperSize(int) -// FitToHeight(int) -// FitToWidth(int) -// PageLayoutScale(uint) +// BlackAndWhite(bool) +// FirstPageNumber(uint) +// PageLayoutOrientation(string) +// PageLayoutPaperSize(int) +// FitToHeight(int) +// FitToWidth(int) +// PageLayoutScale(uint) // // The following shows the paper size sorted by excelize index number: // -// Index | Paper Size -// -------+----------------------------------------------- -// 1 | Letter paper (8.5 in. by 11 in.) -// 2 | Letter small paper (8.5 in. by 11 in.) -// 3 | Tabloid paper (11 in. by 17 in.) -// 4 | Ledger paper (17 in. by 11 in.) -// 5 | Legal paper (8.5 in. by 14 in.) -// 6 | Statement paper (5.5 in. by 8.5 in.) -// 7 | Executive paper (7.25 in. by 10.5 in.) -// 8 | A3 paper (297 mm by 420 mm) -// 9 | A4 paper (210 mm by 297 mm) -// 10 | A4 small paper (210 mm by 297 mm) -// 11 | A5 paper (148 mm by 210 mm) -// 12 | B4 paper (250 mm by 353 mm) -// 13 | B5 paper (176 mm by 250 mm) -// 14 | Folio paper (8.5 in. by 13 in.) -// 15 | Quarto paper (215 mm by 275 mm) -// 16 | Standard paper (10 in. by 14 in.) -// 17 | Standard paper (11 in. by 17 in.) -// 18 | Note paper (8.5 in. by 11 in.) -// 19 | #9 envelope (3.875 in. by 8.875 in.) -// 20 | #10 envelope (4.125 in. by 9.5 in.) -// 21 | #11 envelope (4.5 in. by 10.375 in.) -// 22 | #12 envelope (4.75 in. by 11 in.) -// 23 | #14 envelope (5 in. by 11.5 in.) -// 24 | C paper (17 in. by 22 in.) -// 25 | D paper (22 in. by 34 in.) -// 26 | E paper (34 in. by 44 in.) -// 27 | DL envelope (110 mm by 220 mm) -// 28 | C5 envelope (162 mm by 229 mm) -// 29 | C3 envelope (324 mm by 458 mm) -// 30 | C4 envelope (229 mm by 324 mm) -// 31 | C6 envelope (114 mm by 162 mm) -// 32 | C65 envelope (114 mm by 229 mm) -// 33 | B4 envelope (250 mm by 353 mm) -// 34 | B5 envelope (176 mm by 250 mm) -// 35 | B6 envelope (176 mm by 125 mm) -// 36 | Italy envelope (110 mm by 230 mm) -// 37 | Monarch envelope (3.875 in. by 7.5 in.). -// 38 | 6 3/4 envelope (3.625 in. by 6.5 in.) -// 39 | US standard fanfold (14.875 in. by 11 in.) -// 40 | German standard fanfold (8.5 in. by 12 in.) -// 41 | German legal fanfold (8.5 in. by 13 in.) -// 42 | ISO B4 (250 mm by 353 mm) -// 43 | Japanese postcard (100 mm by 148 mm) -// 44 | Standard paper (9 in. by 11 in.) -// 45 | Standard paper (10 in. by 11 in.) -// 46 | Standard paper (15 in. by 11 in.) -// 47 | Invite envelope (220 mm by 220 mm) -// 50 | Letter extra paper (9.275 in. by 12 in.) -// 51 | Legal extra paper (9.275 in. by 15 in.) -// 52 | Tabloid extra paper (11.69 in. by 18 in.) -// 53 | A4 extra paper (236 mm by 322 mm) -// 54 | Letter transverse paper (8.275 in. by 11 in.) -// 55 | A4 transverse paper (210 mm by 297 mm) -// 56 | Letter extra transverse paper (9.275 in. by 12 in.) -// 57 | SuperA/SuperA/A4 paper (227 mm by 356 mm) -// 58 | SuperB/SuperB/A3 paper (305 mm by 487 mm) -// 59 | Letter plus paper (8.5 in. by 12.69 in.) -// 60 | A4 plus paper (210 mm by 330 mm) -// 61 | A5 transverse paper (148 mm by 210 mm) -// 62 | JIS B5 transverse paper (182 mm by 257 mm) -// 63 | A3 extra paper (322 mm by 445 mm) -// 64 | A5 extra paper (174 mm by 235 mm) -// 65 | ISO B5 extra paper (201 mm by 276 mm) -// 66 | A2 paper (420 mm by 594 mm) -// 67 | A3 transverse paper (297 mm by 420 mm) -// 68 | A3 extra transverse paper (322 mm by 445 mm) -// 69 | Japanese Double Postcard (200 mm x 148 mm) -// 70 | A6 (105 mm x 148 mm) -// 71 | Japanese Envelope Kaku #2 -// 72 | Japanese Envelope Kaku #3 -// 73 | Japanese Envelope Chou #3 -// 74 | Japanese Envelope Chou #4 -// 75 | Letter Rotated (11in x 8 1/2 11 in) -// 76 | A3 Rotated (420 mm x 297 mm) -// 77 | A4 Rotated (297 mm x 210 mm) -// 78 | A5 Rotated (210 mm x 148 mm) -// 79 | B4 (JIS) Rotated (364 mm x 257 mm) -// 80 | B5 (JIS) Rotated (257 mm x 182 mm) -// 81 | Japanese Postcard Rotated (148 mm x 100 mm) -// 82 | Double Japanese Postcard Rotated (148 mm x 200 mm) -// 83 | A6 Rotated (148 mm x 105 mm) -// 84 | Japanese Envelope Kaku #2 Rotated -// 85 | Japanese Envelope Kaku #3 Rotated -// 86 | Japanese Envelope Chou #3 Rotated -// 87 | Japanese Envelope Chou #4 Rotated -// 88 | B6 (JIS) (128 mm x 182 mm) -// 89 | B6 (JIS) Rotated (182 mm x 128 mm) -// 90 | (12 in x 11 in) -// 91 | Japanese Envelope You #4 -// 92 | Japanese Envelope You #4 Rotated -// 93 | PRC 16K (146 mm x 215 mm) -// 94 | PRC 32K (97 mm x 151 mm) -// 95 | PRC 32K(Big) (97 mm x 151 mm) -// 96 | PRC Envelope #1 (102 mm x 165 mm) -// 97 | PRC Envelope #2 (102 mm x 176 mm) -// 98 | PRC Envelope #3 (125 mm x 176 mm) -// 99 | PRC Envelope #4 (110 mm x 208 mm) -// 100 | PRC Envelope #5 (110 mm x 220 mm) -// 101 | PRC Envelope #6 (120 mm x 230 mm) -// 102 | PRC Envelope #7 (160 mm x 230 mm) -// 103 | PRC Envelope #8 (120 mm x 309 mm) -// 104 | PRC Envelope #9 (229 mm x 324 mm) -// 105 | PRC Envelope #10 (324 mm x 458 mm) -// 106 | PRC 16K Rotated -// 107 | PRC 32K Rotated -// 108 | PRC 32K(Big) Rotated -// 109 | PRC Envelope #1 Rotated (165 mm x 102 mm) -// 110 | PRC Envelope #2 Rotated (176 mm x 102 mm) -// 111 | PRC Envelope #3 Rotated (176 mm x 125 mm) -// 112 | PRC Envelope #4 Rotated (208 mm x 110 mm) -// 113 | PRC Envelope #5 Rotated (220 mm x 110 mm) -// 114 | PRC Envelope #6 Rotated (230 mm x 120 mm) -// 115 | PRC Envelope #7 Rotated (230 mm x 160 mm) -// 116 | PRC Envelope #8 Rotated (309 mm x 120 mm) -// 117 | PRC Envelope #9 Rotated (324 mm x 229 mm) -// 118 | PRC Envelope #10 Rotated (458 mm x 324 mm) -// +// Index | Paper Size +// -------+----------------------------------------------- +// 1 | Letter paper (8.5 in. by 11 in.) +// 2 | Letter small paper (8.5 in. by 11 in.) +// 3 | Tabloid paper (11 in. by 17 in.) +// 4 | Ledger paper (17 in. by 11 in.) +// 5 | Legal paper (8.5 in. by 14 in.) +// 6 | Statement paper (5.5 in. by 8.5 in.) +// 7 | Executive paper (7.25 in. by 10.5 in.) +// 8 | A3 paper (297 mm by 420 mm) +// 9 | A4 paper (210 mm by 297 mm) +// 10 | A4 small paper (210 mm by 297 mm) +// 11 | A5 paper (148 mm by 210 mm) +// 12 | B4 paper (250 mm by 353 mm) +// 13 | B5 paper (176 mm by 250 mm) +// 14 | Folio paper (8.5 in. by 13 in.) +// 15 | Quarto paper (215 mm by 275 mm) +// 16 | Standard paper (10 in. by 14 in.) +// 17 | Standard paper (11 in. by 17 in.) +// 18 | Note paper (8.5 in. by 11 in.) +// 19 | #9 envelope (3.875 in. by 8.875 in.) +// 20 | #10 envelope (4.125 in. by 9.5 in.) +// 21 | #11 envelope (4.5 in. by 10.375 in.) +// 22 | #12 envelope (4.75 in. by 11 in.) +// 23 | #14 envelope (5 in. by 11.5 in.) +// 24 | C paper (17 in. by 22 in.) +// 25 | D paper (22 in. by 34 in.) +// 26 | E paper (34 in. by 44 in.) +// 27 | DL envelope (110 mm by 220 mm) +// 28 | C5 envelope (162 mm by 229 mm) +// 29 | C3 envelope (324 mm by 458 mm) +// 30 | C4 envelope (229 mm by 324 mm) +// 31 | C6 envelope (114 mm by 162 mm) +// 32 | C65 envelope (114 mm by 229 mm) +// 33 | B4 envelope (250 mm by 353 mm) +// 34 | B5 envelope (176 mm by 250 mm) +// 35 | B6 envelope (176 mm by 125 mm) +// 36 | Italy envelope (110 mm by 230 mm) +// 37 | Monarch envelope (3.875 in. by 7.5 in.). +// 38 | 6 3/4 envelope (3.625 in. by 6.5 in.) +// 39 | US standard fanfold (14.875 in. by 11 in.) +// 40 | German standard fanfold (8.5 in. by 12 in.) +// 41 | German legal fanfold (8.5 in. by 13 in.) +// 42 | ISO B4 (250 mm by 353 mm) +// 43 | Japanese postcard (100 mm by 148 mm) +// 44 | Standard paper (9 in. by 11 in.) +// 45 | Standard paper (10 in. by 11 in.) +// 46 | Standard paper (15 in. by 11 in.) +// 47 | Invite envelope (220 mm by 220 mm) +// 50 | Letter extra paper (9.275 in. by 12 in.) +// 51 | Legal extra paper (9.275 in. by 15 in.) +// 52 | Tabloid extra paper (11.69 in. by 18 in.) +// 53 | A4 extra paper (236 mm by 322 mm) +// 54 | Letter transverse paper (8.275 in. by 11 in.) +// 55 | A4 transverse paper (210 mm by 297 mm) +// 56 | Letter extra transverse paper (9.275 in. by 12 in.) +// 57 | SuperA/SuperA/A4 paper (227 mm by 356 mm) +// 58 | SuperB/SuperB/A3 paper (305 mm by 487 mm) +// 59 | Letter plus paper (8.5 in. by 12.69 in.) +// 60 | A4 plus paper (210 mm by 330 mm) +// 61 | A5 transverse paper (148 mm by 210 mm) +// 62 | JIS B5 transverse paper (182 mm by 257 mm) +// 63 | A3 extra paper (322 mm by 445 mm) +// 64 | A5 extra paper (174 mm by 235 mm) +// 65 | ISO B5 extra paper (201 mm by 276 mm) +// 66 | A2 paper (420 mm by 594 mm) +// 67 | A3 transverse paper (297 mm by 420 mm) +// 68 | A3 extra transverse paper (322 mm by 445 mm) +// 69 | Japanese Double Postcard (200 mm x 148 mm) +// 70 | A6 (105 mm x 148 mm) +// 71 | Japanese Envelope Kaku #2 +// 72 | Japanese Envelope Kaku #3 +// 73 | Japanese Envelope Chou #3 +// 74 | Japanese Envelope Chou #4 +// 75 | Letter Rotated (11in x 8 1/2 11 in) +// 76 | A3 Rotated (420 mm x 297 mm) +// 77 | A4 Rotated (297 mm x 210 mm) +// 78 | A5 Rotated (210 mm x 148 mm) +// 79 | B4 (JIS) Rotated (364 mm x 257 mm) +// 80 | B5 (JIS) Rotated (257 mm x 182 mm) +// 81 | Japanese Postcard Rotated (148 mm x 100 mm) +// 82 | Double Japanese Postcard Rotated (148 mm x 200 mm) +// 83 | A6 Rotated (148 mm x 105 mm) +// 84 | Japanese Envelope Kaku #2 Rotated +// 85 | Japanese Envelope Kaku #3 Rotated +// 86 | Japanese Envelope Chou #3 Rotated +// 87 | Japanese Envelope Chou #4 Rotated +// 88 | B6 (JIS) (128 mm x 182 mm) +// 89 | B6 (JIS) Rotated (182 mm x 128 mm) +// 90 | (12 in x 11 in) +// 91 | Japanese Envelope You #4 +// 92 | Japanese Envelope You #4 Rotated +// 93 | PRC 16K (146 mm x 215 mm) +// 94 | PRC 32K (97 mm x 151 mm) +// 95 | PRC 32K(Big) (97 mm x 151 mm) +// 96 | PRC Envelope #1 (102 mm x 165 mm) +// 97 | PRC Envelope #2 (102 mm x 176 mm) +// 98 | PRC Envelope #3 (125 mm x 176 mm) +// 99 | PRC Envelope #4 (110 mm x 208 mm) +// 100 | PRC Envelope #5 (110 mm x 220 mm) +// 101 | PRC Envelope #6 (120 mm x 230 mm) +// 102 | PRC Envelope #7 (160 mm x 230 mm) +// 103 | PRC Envelope #8 (120 mm x 309 mm) +// 104 | PRC Envelope #9 (229 mm x 324 mm) +// 105 | PRC Envelope #10 (324 mm x 458 mm) +// 106 | PRC 16K Rotated +// 107 | PRC 32K Rotated +// 108 | PRC 32K(Big) Rotated +// 109 | PRC Envelope #1 Rotated (165 mm x 102 mm) +// 110 | PRC Envelope #2 Rotated (176 mm x 102 mm) +// 111 | PRC Envelope #3 Rotated (176 mm x 125 mm) +// 112 | PRC Envelope #4 Rotated (208 mm x 110 mm) +// 113 | PRC Envelope #5 Rotated (220 mm x 110 mm) +// 114 | PRC Envelope #6 Rotated (230 mm x 120 mm) +// 115 | PRC Envelope #7 Rotated (230 mm x 160 mm) +// 116 | PRC Envelope #8 Rotated (309 mm x 120 mm) +// 117 | PRC Envelope #9 Rotated (324 mm x 229 mm) +// 118 | PRC Envelope #10 Rotated (458 mm x 324 mm) func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error { s, err := f.workSheetReader(sheet) if err != nil { @@ -1526,10 +1517,11 @@ func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error { // GetPageLayout provides a function to gets worksheet page layout. // // Available options: -// PageLayoutOrientation(string) -// PageLayoutPaperSize(int) -// FitToHeight(int) -// FitToWidth(int) +// +// PageLayoutOrientation(string) +// PageLayoutPaperSize(int) +// FitToHeight(int) +// FitToWidth(int) func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { s, err := f.workSheetReader(sheet) if err != nil { @@ -1547,13 +1539,12 @@ func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { // or worksheet. If not specified scope, the default scope is workbook. // For example: // -// f.SetDefinedName(&excelize.DefinedName{ -// Name: "Amount", -// RefersTo: "Sheet1!$A$2:$D$5", -// Comment: "defined name comment", -// Scope: "Sheet2", -// }) -// +// f.SetDefinedName(&excelize.DefinedName{ +// Name: "Amount", +// RefersTo: "Sheet1!$A$2:$D$5", +// Comment: "defined name comment", +// Scope: "Sheet2", +// }) func (f *File) SetDefinedName(definedName *DefinedName) error { wb := f.workbookReader() d := xlsxDefinedName{ @@ -1589,11 +1580,10 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { // workbook or worksheet. If not specified scope, the default scope is // workbook. For example: // -// f.DeleteDefinedName(&excelize.DefinedName{ -// Name: "Amount", -// Scope: "Sheet2", -// }) -// +// f.DeleteDefinedName(&excelize.DefinedName{ +// Name: "Amount", +// Scope: "Sheet2", +// }) func (f *File) DeleteDefinedName(definedName *DefinedName) error { wb := f.workbookReader() if wb.DefinedNames != nil { diff --git a/sheetpr.go b/sheetpr.go index 65939c16d0..8675c7548a 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -238,16 +238,17 @@ func (o *AutoPageBreaks) getSheetPrOption(pr *xlsxSheetPr) { // SetSheetPrOptions provides a function to sets worksheet properties. // // Available options: -// CodeName(string) -// EnableFormatConditionsCalculation(bool) -// Published(bool) -// FitToPage(bool) -// TabColorIndexed(int) -// TabColorRGB(string) -// TabColorTheme(int) -// TabColorTint(float64) -// AutoPageBreaks(bool) -// OutlineSummaryBelow(bool) +// +// CodeName(string) +// EnableFormatConditionsCalculation(bool) +// Published(bool) +// FitToPage(bool) +// TabColorIndexed(int) +// TabColorRGB(string) +// TabColorTheme(int) +// TabColorTint(float64) +// AutoPageBreaks(bool) +// OutlineSummaryBelow(bool) func (f *File) SetSheetPrOptions(sheet string, opts ...SheetPrOption) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -268,16 +269,17 @@ func (f *File) SetSheetPrOptions(sheet string, opts ...SheetPrOption) error { // GetSheetPrOptions provides a function to gets worksheet properties. // // Available options: -// CodeName(string) -// EnableFormatConditionsCalculation(bool) -// Published(bool) -// FitToPage(bool) -// TabColorIndexed(int) -// TabColorRGB(string) -// TabColorTheme(int) -// TabColorTint(float64) -// AutoPageBreaks(bool) -// OutlineSummaryBelow(bool) +// +// CodeName(string) +// EnableFormatConditionsCalculation(bool) +// Published(bool) +// FitToPage(bool) +// TabColorIndexed(int) +// TabColorRGB(string) +// TabColorTheme(int) +// TabColorTint(float64) +// AutoPageBreaks(bool) +// OutlineSummaryBelow(bool) func (f *File) GetSheetPrOptions(sheet string, opts ...SheetPrOptionPtr) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -412,12 +414,13 @@ type PageMarginsOptionsPtr interface { // SetPageMargins provides a function to set worksheet page margins. // // Available options: -// PageMarginBottom(float64) -// PageMarginFooter(float64) -// PageMarginHeader(float64) -// PageMarginLeft(float64) -// PageMarginRight(float64) -// PageMarginTop(float64) +// +// PageMarginBottom(float64) +// PageMarginFooter(float64) +// PageMarginHeader(float64) +// PageMarginLeft(float64) +// PageMarginRight(float64) +// PageMarginTop(float64) func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error { s, err := f.workSheetReader(sheet) if err != nil { @@ -438,12 +441,13 @@ func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error { // GetPageMargins provides a function to get worksheet page margins. // // Available options: -// PageMarginBottom(float64) -// PageMarginFooter(float64) -// PageMarginHeader(float64) -// PageMarginLeft(float64) -// PageMarginRight(float64) -// PageMarginTop(float64) +// +// PageMarginBottom(float64) +// PageMarginFooter(float64) +// PageMarginHeader(float64) +// PageMarginLeft(float64) +// PageMarginRight(float64) +// PageMarginTop(float64) func (f *File) GetPageMargins(sheet string, opts ...PageMarginsOptionsPtr) error { s, err := f.workSheetReader(sheet) if err != nil { @@ -605,13 +609,14 @@ func (p *ThickBottom) getSheetFormatPr(fp *xlsxSheetFormatPr) { // SetSheetFormatPr provides a function to set worksheet formatting properties. // // Available options: -// BaseColWidth(uint8) -// DefaultColWidth(float64) -// DefaultRowHeight(float64) -// CustomHeight(bool) -// ZeroHeight(bool) -// ThickTop(bool) -// ThickBottom(bool) +// +// BaseColWidth(uint8) +// DefaultColWidth(float64) +// DefaultRowHeight(float64) +// CustomHeight(bool) +// ZeroHeight(bool) +// ThickTop(bool) +// ThickBottom(bool) func (f *File) SetSheetFormatPr(sheet string, opts ...SheetFormatPrOptions) error { s, err := f.workSheetReader(sheet) if err != nil { @@ -631,13 +636,14 @@ func (f *File) SetSheetFormatPr(sheet string, opts ...SheetFormatPrOptions) erro // GetSheetFormatPr provides a function to get worksheet formatting properties. // // Available options: -// BaseColWidth(uint8) -// DefaultColWidth(float64) -// DefaultRowHeight(float64) -// CustomHeight(bool) -// ZeroHeight(bool) -// ThickTop(bool) -// ThickBottom(bool) +// +// BaseColWidth(uint8) +// DefaultColWidth(float64) +// DefaultRowHeight(float64) +// CustomHeight(bool) +// ZeroHeight(bool) +// ThickTop(bool) +// ThickBottom(bool) func (f *File) GetSheetFormatPr(sheet string, opts ...SheetFormatPrOptionsPtr) error { s, err := f.workSheetReader(sheet) if err != nil { diff --git a/sheetview.go b/sheetview.go index 05312ca1ed..373658844c 100644 --- a/sheetview.go +++ b/sheetview.go @@ -185,21 +185,20 @@ func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) // // Available options: // -// DefaultGridColor(bool) -// ShowFormulas(bool) -// ShowGridLines(bool) -// ShowRowColHeaders(bool) -// ShowZeros(bool) -// RightToLeft(bool) -// ShowRuler(bool) -// View(string) -// TopLeftCell(string) -// ZoomScale(float64) +// DefaultGridColor(bool) +// ShowFormulas(bool) +// ShowGridLines(bool) +// ShowRowColHeaders(bool) +// ShowZeros(bool) +// RightToLeft(bool) +// ShowRuler(bool) +// View(string) +// TopLeftCell(string) +// ZoomScale(float64) // // Example: // -// err = f.SetSheetViewOptions("Sheet1", -1, ShowGridLines(false)) -// +// err = f.SetSheetViewOptions("Sheet1", -1, ShowGridLines(false)) func (f *File) SetSheetViewOptions(sheet string, viewIndex int, opts ...SheetViewOption) error { view, err := f.getSheetView(sheet, viewIndex) if err != nil { @@ -217,22 +216,21 @@ func (f *File) SetSheetViewOptions(sheet string, viewIndex int, opts ...SheetVie // // Available options: // -// DefaultGridColor(bool) -// ShowFormulas(bool) -// ShowGridLines(bool) -// ShowRowColHeaders(bool) -// ShowZeros(bool) -// RightToLeft(bool) -// ShowRuler(bool) -// View(string) -// TopLeftCell(string) -// ZoomScale(float64) +// DefaultGridColor(bool) +// ShowFormulas(bool) +// ShowGridLines(bool) +// ShowRowColHeaders(bool) +// ShowZeros(bool) +// RightToLeft(bool) +// ShowRuler(bool) +// View(string) +// TopLeftCell(string) +// ZoomScale(float64) // // Example: // -// var showGridLines excelize.ShowGridLines -// err = f.GetSheetViewOptions("Sheet1", -1, &showGridLines) -// +// var showGridLines excelize.ShowGridLines +// err = f.GetSheetViewOptions("Sheet1", -1, &showGridLines) func (f *File) GetSheetViewOptions(sheet string, viewIndex int, opts ...SheetViewOptionPtr) error { view, err := f.getSheetView(sheet, viewIndex) if err != nil { diff --git a/sparkline.go b/sparkline.go index 880724a47b..bf1e09cd94 100644 --- a/sparkline.go +++ b/sparkline.go @@ -365,29 +365,28 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // Excel 2007, but they won't be displayed. For example, add a grouped // sparkline. Changes are applied to all three: // -// err := f.AddSparkline("Sheet1", &excelize.SparklineOption{ -// Location: []string{"A1", "A2", "A3"}, -// Range: []string{"Sheet2!A1:J1", "Sheet2!A2:J2", "Sheet2!A3:J3"}, -// Markers: true, -// }) +// err := f.AddSparkline("Sheet1", &excelize.SparklineOption{ +// Location: []string{"A1", "A2", "A3"}, +// Range: []string{"Sheet2!A1:J1", "Sheet2!A2:J2", "Sheet2!A3:J3"}, +// Markers: true, +// }) // // The following shows the formatting options of sparkline supported by excelize: // -// Parameter | Description -// -----------+-------------------------------------------- -// Location | Required, must have the same number with 'Range' parameter -// Range | Required, must have the same number with 'Location' parameter -// Type | Enumeration value: line, column, win_loss -// Style | Value range: 0 - 35 -// Hight | Toggle sparkline high points -// Low | Toggle sparkline low points -// First | Toggle sparkline first points -// Last | Toggle sparkline last points -// Negative | Toggle sparkline negative points -// Markers | Toggle sparkline markers -// ColorAxis | An RGB Color is specified as RRGGBB -// Axis | Show sparkline axis -// +// Parameter | Description +// -----------+-------------------------------------------- +// Location | Required, must have the same number with 'Range' parameter +// Range | Required, must have the same number with 'Location' parameter +// Type | Enumeration value: line, column, win_loss +// Style | Value range: 0 - 35 +// Hight | Toggle sparkline high points +// Low | Toggle sparkline low points +// First | Toggle sparkline first points +// Last | Toggle sparkline last points +// Negative | Toggle sparkline negative points +// Markers | Toggle sparkline markers +// ColorAxis | An RGB Color is specified as RRGGBB +// Axis | Show sparkline axis func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { var ( ws *xlsxWorksheet diff --git a/stream.go b/stream.go index 52e65a46c8..91ae78a45f 100644 --- a/stream.go +++ b/stream.go @@ -47,49 +47,48 @@ type StreamWriter struct { // example, set data for worksheet of size 102400 rows x 50 columns with // numbers and style: // -// file := excelize.NewFile() -// streamWriter, err := file.NewStreamWriter("Sheet1") -// if err != nil { -// fmt.Println(err) -// } -// styleID, err := file.NewStyle(&excelize.Style{Font: &excelize.Font{Color: "#777777"}}) -// if err != nil { -// fmt.Println(err) -// } -// if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}, -// excelize.RowOpts{Height: 45, Hidden: false}); err != nil { -// fmt.Println(err) -// } -// for rowID := 2; rowID <= 102400; rowID++ { -// row := make([]interface{}, 50) -// for colID := 0; colID < 50; colID++ { -// row[colID] = rand.Intn(640000) -// } -// cell, _ := excelize.CoordinatesToCellName(1, rowID) -// if err := streamWriter.SetRow(cell, row); err != nil { -// fmt.Println(err) -// } -// } -// if err := streamWriter.Flush(); err != nil { -// fmt.Println(err) -// } -// if err := file.SaveAs("Book1.xlsx"); err != nil { -// fmt.Println(err) -// } +// file := excelize.NewFile() +// streamWriter, err := file.NewStreamWriter("Sheet1") +// if err != nil { +// fmt.Println(err) +// } +// styleID, err := file.NewStyle(&excelize.Style{Font: &excelize.Font{Color: "#777777"}}) +// if err != nil { +// fmt.Println(err) +// } +// if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}, +// excelize.RowOpts{Height: 45, Hidden: false}); err != nil { +// fmt.Println(err) +// } +// for rowID := 2; rowID <= 102400; rowID++ { +// row := make([]interface{}, 50) +// for colID := 0; colID < 50; colID++ { +// row[colID] = rand.Intn(640000) +// } +// cell, _ := excelize.CoordinatesToCellName(1, rowID) +// if err := streamWriter.SetRow(cell, row); err != nil { +// fmt.Println(err) +// } +// } +// if err := streamWriter.Flush(); err != nil { +// fmt.Println(err) +// } +// if err := file.SaveAs("Book1.xlsx"); err != nil { +// fmt.Println(err) +// } // // Set cell value and cell formula for a worksheet with stream writer: // -// err := streamWriter.SetRow("A1", []interface{}{ -// excelize.Cell{Value: 1}, -// excelize.Cell{Value: 2}, -// excelize.Cell{Formula: "SUM(A1,B1)"}}); +// err := streamWriter.SetRow("A1", []interface{}{ +// excelize.Cell{Value: 1}, +// excelize.Cell{Value: 2}, +// excelize.Cell{Formula: "SUM(A1,B1)"}}); // // Set cell value and rows style for a worksheet with stream writer: // -// err := streamWriter.SetRow("A1", []interface{}{ -// excelize.Cell{Value: 1}}, -// excelize.RowOpts{StyleID: styleID, Height: 20, Hidden: false}); -// +// err := streamWriter.SetRow("A1", []interface{}{ +// excelize.Cell{Value: 1}}, +// excelize.RowOpts{StyleID: styleID, Height: 20, Hidden: false}); func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { sheetID := f.getSheetID(sheet) if sheetID == -1 { @@ -120,18 +119,18 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { // AddTable creates an Excel table for the StreamWriter using the given // coordinate area and format set. For example, create a table of A1:D5: // -// err := sw.AddTable("A1", "D5", "") +// err := sw.AddTable("A1", "D5", "") // // Create a table of F2:H6 with format set: // -// err := sw.AddTable("F2", "H6", `{ -// "table_name": "table", -// "table_style": "TableStyleMedium2", -// "show_first_column": true, -// "show_last_column": true, -// "show_row_stripes": false, -// "show_column_stripes": true -// }`) +// err := sw.AddTable("F2", "H6", `{ +// "table_name": "table", +// "table_style": "TableStyleMedium2", +// "show_first_column": true, +// "show_last_column": true, +// "show_row_stripes": false, +// "show_column_stripes": true +// }`) // // Note that the table must be at least two lines including the header. The // header cells must contain strings and must be unique. @@ -384,8 +383,7 @@ func marshalRowAttrs(opts ...RowOpts) (attrs string, err error) { // the 'SetColWidth' function before the 'SetRow' function. For example set // the width column B:C as 20: // -// err := streamWriter.SetColWidth(2, 3, 20) -// +// err := streamWriter.SetColWidth(2, 3, 20) func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { if sw.sheetWritten { return ErrStreamSetColWidth diff --git a/styles.go b/styles.go index 0220e9c976..8eb0587884 100644 --- a/styles.go +++ b/styles.go @@ -985,863 +985,862 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { // // The following shows the border styles sorted by excelize index number: // -// Index | Name | Weight | Style -// -------+---------------+--------+------------- -// 0 | None | 0 | -// 1 | Continuous | 1 | ----------- -// 2 | Continuous | 2 | ----------- -// 3 | Dash | 1 | - - - - - - -// 4 | Dot | 1 | . . . . . . -// 5 | Continuous | 3 | ----------- -// 6 | Double | 3 | =========== -// 7 | Continuous | 0 | ----------- -// 8 | Dash | 2 | - - - - - - -// 9 | Dash Dot | 1 | - . - . - . -// 10 | Dash Dot | 2 | - . - . - . -// 11 | Dash Dot Dot | 1 | - . . - . . -// 12 | Dash Dot Dot | 2 | - . . - . . -// 13 | SlantDash Dot | 2 | / - . / - . +// Index | Name | Weight | Style +// -------+---------------+--------+------------- +// 0 | None | 0 | +// 1 | Continuous | 1 | ----------- +// 2 | Continuous | 2 | ----------- +// 3 | Dash | 1 | - - - - - - +// 4 | Dot | 1 | . . . . . . +// 5 | Continuous | 3 | ----------- +// 6 | Double | 3 | =========== +// 7 | Continuous | 0 | ----------- +// 8 | Dash | 2 | - - - - - - +// 9 | Dash Dot | 1 | - . - . - . +// 10 | Dash Dot | 2 | - . - . - . +// 11 | Dash Dot Dot | 1 | - . . - . . +// 12 | Dash Dot Dot | 2 | - . . - . . +// 13 | SlantDash Dot | 2 | / - . / - . // // The following shows the borders in the order shown in the Excel dialog: // -// Index | Style | Index | Style -// -------+-------------+-------+------------- -// 0 | None | 12 | - . . - . . -// 7 | ----------- | 13 | / - . / - . -// 4 | . . . . . . | 10 | - . - . - . -// 11 | - . . - . . | 8 | - - - - - - -// 9 | - . - . - . | 2 | ----------- -// 3 | - - - - - - | 5 | ----------- -// 1 | ----------- | 6 | =========== +// Index | Style | Index | Style +// -------+-------------+-------+------------- +// 0 | None | 12 | - . . - . . +// 7 | ----------- | 13 | / - . / - . +// 4 | . . . . . . | 10 | - . - . - . +// 11 | - . . - . . | 8 | - - - - - - +// 9 | - . - . - . | 2 | ----------- +// 3 | - - - - - - | 5 | ----------- +// 1 | ----------- | 6 | =========== // // The following shows the shading styles sorted by excelize index number: // -// Index | Style | Index | Style -// -------+-----------------+-------+----------------- -// 0 | Horizontal | 3 | Diagonal down -// 1 | Vertical | 4 | From corner -// 2 | Diagonal Up | 5 | From center +// Index | Style | Index | Style +// -------+-----------------+-------+----------------- +// 0 | Horizontal | 3 | Diagonal down +// 1 | Vertical | 4 | From corner +// 2 | Diagonal Up | 5 | From center // // The following shows the patterns styles sorted by excelize index number: // -// Index | Style | Index | Style -// -------+-----------------+-------+----------------- -// 0 | None | 10 | darkTrellis -// 1 | solid | 11 | lightHorizontal -// 2 | mediumGray | 12 | lightVertical -// 3 | darkGray | 13 | lightDown -// 4 | lightGray | 14 | lightUp -// 5 | darkHorizontal | 15 | lightGrid -// 6 | darkVertical | 16 | lightTrellis -// 7 | darkDown | 17 | gray125 -// 8 | darkUp | 18 | gray0625 -// 9 | darkGrid | | +// Index | Style | Index | Style +// -------+-----------------+-------+----------------- +// 0 | None | 10 | darkTrellis +// 1 | solid | 11 | lightHorizontal +// 2 | mediumGray | 12 | lightVertical +// 3 | darkGray | 13 | lightDown +// 4 | lightGray | 14 | lightUp +// 5 | darkHorizontal | 15 | lightGrid +// 6 | darkVertical | 16 | lightTrellis +// 7 | darkDown | 17 | gray125 +// 8 | darkUp | 18 | gray0625 +// 9 | darkGrid | | // // The following the type of horizontal alignment in cells: // -// Style -// ------------------ -// left -// center -// right -// fill -// justify -// centerContinuous -// distributed +// Style +// ------------------ +// left +// center +// right +// fill +// justify +// centerContinuous +// distributed // // The following the type of vertical alignment in cells: // -// Style -// ------------------ -// top -// center -// justify -// distributed +// Style +// ------------------ +// top +// center +// justify +// distributed // // The following the type of font underline style: // -// Style -// ------------------ -// single -// double +// Style +// ------------------ +// single +// double // // Excel's built-in all languages formats are shown in the following table: // -// Index | Format String -// -------+---------------------------------------------------- -// 0 | General -// 1 | 0 -// 2 | 0.00 -// 3 | #,##0 -// 4 | #,##0.00 -// 5 | ($#,##0_);($#,##0) -// 6 | ($#,##0_);[Red]($#,##0) -// 7 | ($#,##0.00_);($#,##0.00) -// 8 | ($#,##0.00_);[Red]($#,##0.00) -// 9 | 0% -// 10 | 0.00% -// 11 | 0.00E+00 -// 12 | # ?/? -// 13 | # ??/?? -// 14 | m/d/yy -// 15 | d-mmm-yy -// 16 | d-mmm -// 17 | mmm-yy -// 18 | h:mm AM/PM -// 19 | h:mm:ss AM/PM -// 20 | h:mm -// 21 | h:mm:ss -// 22 | m/d/yy h:mm -// ... | ... -// 37 | (#,##0_);(#,##0) -// 38 | (#,##0_);[Red](#,##0) -// 39 | (#,##0.00_);(#,##0.00) -// 40 | (#,##0.00_);[Red](#,##0.00) -// 41 | _(* #,##0_);_(* (#,##0);_(* "-"_);_(@_) -// 42 | _($* #,##0_);_($* (#,##0);_($* "-"_);_(@_) -// 43 | _(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_) -// 44 | _($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_) -// 45 | mm:ss -// 46 | [h]:mm:ss -// 47 | mm:ss.0 -// 48 | ##0.0E+0 -// 49 | @ +// Index | Format String +// -------+---------------------------------------------------- +// 0 | General +// 1 | 0 +// 2 | 0.00 +// 3 | #,##0 +// 4 | #,##0.00 +// 5 | ($#,##0_);($#,##0) +// 6 | ($#,##0_);[Red]($#,##0) +// 7 | ($#,##0.00_);($#,##0.00) +// 8 | ($#,##0.00_);[Red]($#,##0.00) +// 9 | 0% +// 10 | 0.00% +// 11 | 0.00E+00 +// 12 | # ?/? +// 13 | # ??/?? +// 14 | m/d/yy +// 15 | d-mmm-yy +// 16 | d-mmm +// 17 | mmm-yy +// 18 | h:mm AM/PM +// 19 | h:mm:ss AM/PM +// 20 | h:mm +// 21 | h:mm:ss +// 22 | m/d/yy h:mm +// ... | ... +// 37 | (#,##0_);(#,##0) +// 38 | (#,##0_);[Red](#,##0) +// 39 | (#,##0.00_);(#,##0.00) +// 40 | (#,##0.00_);[Red](#,##0.00) +// 41 | _(* #,##0_);_(* (#,##0);_(* "-"_);_(@_) +// 42 | _($* #,##0_);_($* (#,##0);_($* "-"_);_(@_) +// 43 | _(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_) +// 44 | _($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_) +// 45 | mm:ss +// 46 | [h]:mm:ss +// 47 | mm:ss.0 +// 48 | ##0.0E+0 +// 49 | @ // // Number format code in zh-tw language: // -// Index | Symbol -// -------+------------------------------------------- -// 27 | [$-404]e/m/d -// 28 | [$-404]e"年"m"月"d"日" -// 29 | [$-404]e"年"m"月"d"日" -// 30 | m/d/yy -// 31 | yyyy"年"m"月"d"日" -// 32 | hh"時"mm"分" -// 33 | hh"時"mm"分"ss"秒" -// 34 | 上午/下午 hh"時"mm"分" -// 35 | 上午/下午 hh"時"mm"分"ss"秒" -// 36 | [$-404]e/m/d -// 50 | [$-404]e/m/d -// 51 | [$-404]e"年"m"月"d"日" -// 52 | 上午/下午 hh"時"mm"分" -// 53 | 上午/下午 hh"時"mm"分"ss"秒" -// 54 | [$-404]e"年"m"月"d"日" -// 55 | 上午/下午 hh"時"mm"分" -// 56 | 上午/下午 hh"時"mm"分"ss"秒" -// 57 | [$-404]e/m/d -// 58 | [$-404]e"年"m"月"d"日" +// Index | Symbol +// -------+------------------------------------------- +// 27 | [$-404]e/m/d +// 28 | [$-404]e"年"m"月"d"日" +// 29 | [$-404]e"年"m"月"d"日" +// 30 | m/d/yy +// 31 | yyyy"年"m"月"d"日" +// 32 | hh"時"mm"分" +// 33 | hh"時"mm"分"ss"秒" +// 34 | 上午/下午 hh"時"mm"分" +// 35 | 上午/下午 hh"時"mm"分"ss"秒" +// 36 | [$-404]e/m/d +// 50 | [$-404]e/m/d +// 51 | [$-404]e"年"m"月"d"日" +// 52 | 上午/下午 hh"時"mm"分" +// 53 | 上午/下午 hh"時"mm"分"ss"秒" +// 54 | [$-404]e"年"m"月"d"日" +// 55 | 上午/下午 hh"時"mm"分" +// 56 | 上午/下午 hh"時"mm"分"ss"秒" +// 57 | [$-404]e/m/d +// 58 | [$-404]e"年"m"月"d"日" // // Number format code in zh-cn language: // -// Index | Symbol -// -------+------------------------------------------- -// 27 | yyyy"年"m"月" -// 28 | m"月"d"日" -// 29 | m"月"d"日" -// 30 | m-d-yy -// 31 | yyyy"年"m"月"d"日" -// 32 | h"时"mm"分" -// 33 | h"时"mm"分"ss"秒" -// 34 | 上午/下午 h"时"mm"分" -// 35 | 上午/下午 h"时"mm"分"ss"秒 -// 36 | yyyy"年"m"月 -// 50 | yyyy"年"m"月 -// 51 | m"月"d"日 -// 52 | yyyy"年"m"月 -// 53 | m"月"d"日 -// 54 | m"月"d"日 -// 55 | 上午/下午 h"时"mm"分 -// 56 | 上午/下午 h"时"mm"分"ss"秒 -// 57 | yyyy"年"m"月 -// 58 | m"月"d"日" +// Index | Symbol +// -------+------------------------------------------- +// 27 | yyyy"年"m"月" +// 28 | m"月"d"日" +// 29 | m"月"d"日" +// 30 | m-d-yy +// 31 | yyyy"年"m"月"d"日" +// 32 | h"时"mm"分" +// 33 | h"时"mm"分"ss"秒" +// 34 | 上午/下午 h"时"mm"分" +// 35 | 上午/下午 h"时"mm"分"ss"秒 +// 36 | yyyy"年"m"月 +// 50 | yyyy"年"m"月 +// 51 | m"月"d"日 +// 52 | yyyy"年"m"月 +// 53 | m"月"d"日 +// 54 | m"月"d"日 +// 55 | 上午/下午 h"时"mm"分 +// 56 | 上午/下午 h"时"mm"分"ss"秒 +// 57 | yyyy"年"m"月 +// 58 | m"月"d"日" // // Number format code with unicode values provided for language glyphs where // they occur in zh-tw language: // -// Index | Symbol -// -------+------------------------------------------- -// 27 | [$-404]e/m/ -// 28 | [$-404]e"5E74"m"6708"d"65E5 -// 29 | [$-404]e"5E74"m"6708"d"65E5 -// 30 | m/d/y -// 31 | yyyy"5E74"m"6708"d"65E5 -// 32 | hh"6642"mm"5206 -// 33 | hh"6642"mm"5206"ss"79D2 -// 34 | 4E0A5348/4E0B5348hh"6642"mm"5206 -// 35 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2 -// 36 | [$-404]e/m/ -// 50 | [$-404]e/m/ -// 51 | [$-404]e"5E74"m"6708"d"65E5 -// 52 | 4E0A5348/4E0B5348hh"6642"mm"5206 -// 53 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2 -// 54 | [$-404]e"5E74"m"6708"d"65E5 -// 55 | 4E0A5348/4E0B5348hh"6642"mm"5206 -// 56 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2 -// 57 | [$-404]e/m/ -// 58 | [$-404]e"5E74"m"6708"d"65E5" +// Index | Symbol +// -------+------------------------------------------- +// 27 | [$-404]e/m/ +// 28 | [$-404]e"5E74"m"6708"d"65E5 +// 29 | [$-404]e"5E74"m"6708"d"65E5 +// 30 | m/d/y +// 31 | yyyy"5E74"m"6708"d"65E5 +// 32 | hh"6642"mm"5206 +// 33 | hh"6642"mm"5206"ss"79D2 +// 34 | 4E0A5348/4E0B5348hh"6642"mm"5206 +// 35 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2 +// 36 | [$-404]e/m/ +// 50 | [$-404]e/m/ +// 51 | [$-404]e"5E74"m"6708"d"65E5 +// 52 | 4E0A5348/4E0B5348hh"6642"mm"5206 +// 53 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2 +// 54 | [$-404]e"5E74"m"6708"d"65E5 +// 55 | 4E0A5348/4E0B5348hh"6642"mm"5206 +// 56 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2 +// 57 | [$-404]e/m/ +// 58 | [$-404]e"5E74"m"6708"d"65E5" // // Number format code with unicode values provided for language glyphs where // they occur in zh-cn language: // -// Index | Symbol -// -------+------------------------------------------- -// 27 | yyyy"5E74"m"6708 -// 28 | m"6708"d"65E5 -// 29 | m"6708"d"65E5 -// 30 | m-d-y -// 31 | yyyy"5E74"m"6708"d"65E5 -// 32 | h"65F6"mm"5206 -// 33 | h"65F6"mm"5206"ss"79D2 -// 34 | 4E0A5348/4E0B5348h"65F6"mm"5206 -// 35 | 4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2 -// 36 | yyyy"5E74"m"6708 -// 50 | yyyy"5E74"m"6708 -// 51 | m"6708"d"65E5 -// 52 | yyyy"5E74"m"6708 -// 53 | m"6708"d"65E5 -// 54 | m"6708"d"65E5 -// 55 | 4E0A5348/4E0B5348h"65F6"mm"5206 -// 56 | 4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2 -// 57 | yyyy"5E74"m"6708 -// 58 | m"6708"d"65E5" +// Index | Symbol +// -------+------------------------------------------- +// 27 | yyyy"5E74"m"6708 +// 28 | m"6708"d"65E5 +// 29 | m"6708"d"65E5 +// 30 | m-d-y +// 31 | yyyy"5E74"m"6708"d"65E5 +// 32 | h"65F6"mm"5206 +// 33 | h"65F6"mm"5206"ss"79D2 +// 34 | 4E0A5348/4E0B5348h"65F6"mm"5206 +// 35 | 4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2 +// 36 | yyyy"5E74"m"6708 +// 50 | yyyy"5E74"m"6708 +// 51 | m"6708"d"65E5 +// 52 | yyyy"5E74"m"6708 +// 53 | m"6708"d"65E5 +// 54 | m"6708"d"65E5 +// 55 | 4E0A5348/4E0B5348h"65F6"mm"5206 +// 56 | 4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2 +// 57 | yyyy"5E74"m"6708 +// 58 | m"6708"d"65E5" // // Number format code in ja-jp language: // -// Index | Symbol -// -------+------------------------------------------- -// 27 | [$-411]ge.m.d -// 28 | [$-411]ggge"年"m"月"d"日 -// 29 | [$-411]ggge"年"m"月"d"日 -// 30 | m/d/y -// 31 | yyyy"年"m"月"d"日 -// 32 | h"時"mm"分 -// 33 | h"時"mm"分"ss"秒 -// 34 | yyyy"年"m"月 -// 35 | m"月"d"日 -// 36 | [$-411]ge.m.d -// 50 | [$-411]ge.m.d -// 51 | [$-411]ggge"年"m"月"d"日 -// 52 | yyyy"年"m"月 -// 53 | m"月"d"日 -// 54 | [$-411]ggge"年"m"月"d"日 -// 55 | yyyy"年"m"月 -// 56 | m"月"d"日 -// 57 | [$-411]ge.m.d -// 58 | [$-411]ggge"年"m"月"d"日" +// Index | Symbol +// -------+------------------------------------------- +// 27 | [$-411]ge.m.d +// 28 | [$-411]ggge"年"m"月"d"日 +// 29 | [$-411]ggge"年"m"月"d"日 +// 30 | m/d/y +// 31 | yyyy"年"m"月"d"日 +// 32 | h"時"mm"分 +// 33 | h"時"mm"分"ss"秒 +// 34 | yyyy"年"m"月 +// 35 | m"月"d"日 +// 36 | [$-411]ge.m.d +// 50 | [$-411]ge.m.d +// 51 | [$-411]ggge"年"m"月"d"日 +// 52 | yyyy"年"m"月 +// 53 | m"月"d"日 +// 54 | [$-411]ggge"年"m"月"d"日 +// 55 | yyyy"年"m"月 +// 56 | m"月"d"日 +// 57 | [$-411]ge.m.d +// 58 | [$-411]ggge"年"m"月"d"日" // // Number format code in ko-kr language: // -// Index | Symbol -// -------+------------------------------------------- -// 27 | yyyy"年" mm"月" dd"日 -// 28 | mm-d -// 29 | mm-d -// 30 | mm-dd-y -// 31 | yyyy"년" mm"월" dd"일 -// 32 | h"시" mm"분 -// 33 | h"시" mm"분" ss"초 -// 34 | yyyy-mm-d -// 35 | yyyy-mm-d -// 36 | yyyy"年" mm"月" dd"日 -// 50 | yyyy"年" mm"月" dd"日 -// 51 | mm-d -// 52 | yyyy-mm-d -// 53 | yyyy-mm-d -// 54 | mm-d -// 55 | yyyy-mm-d -// 56 | yyyy-mm-d -// 57 | yyyy"年" mm"月" dd"日 -// 58 | mm-dd +// Index | Symbol +// -------+------------------------------------------- +// 27 | yyyy"年" mm"月" dd"日 +// 28 | mm-d +// 29 | mm-d +// 30 | mm-dd-y +// 31 | yyyy"년" mm"월" dd"일 +// 32 | h"시" mm"분 +// 33 | h"시" mm"분" ss"초 +// 34 | yyyy-mm-d +// 35 | yyyy-mm-d +// 36 | yyyy"年" mm"月" dd"日 +// 50 | yyyy"年" mm"月" dd"日 +// 51 | mm-d +// 52 | yyyy-mm-d +// 53 | yyyy-mm-d +// 54 | mm-d +// 55 | yyyy-mm-d +// 56 | yyyy-mm-d +// 57 | yyyy"年" mm"月" dd"日 +// 58 | mm-dd // // Number format code with unicode values provided for language glyphs where // they occur in ja-jp language: // -// Index | Symbol -// -------+------------------------------------------- -// 27 | [$-411]ge.m.d -// 28 | [$-411]ggge"5E74"m"6708"d"65E5 -// 29 | [$-411]ggge"5E74"m"6708"d"65E5 -// 30 | m/d/y -// 31 | yyyy"5E74"m"6708"d"65E5 -// 32 | h"6642"mm"5206 -// 33 | h"6642"mm"5206"ss"79D2 -// 34 | yyyy"5E74"m"6708 -// 35 | m"6708"d"65E5 -// 36 | [$-411]ge.m.d -// 50 | [$-411]ge.m.d -// 51 | [$-411]ggge"5E74"m"6708"d"65E5 -// 52 | yyyy"5E74"m"6708 -// 53 | m"6708"d"65E5 -// 54 | [$-411]ggge"5E74"m"6708"d"65E5 -// 55 | yyyy"5E74"m"6708 -// 56 | m"6708"d"65E5 -// 57 | [$-411]ge.m.d -// 58 | [$-411]ggge"5E74"m"6708"d"65E5" +// Index | Symbol +// -------+------------------------------------------- +// 27 | [$-411]ge.m.d +// 28 | [$-411]ggge"5E74"m"6708"d"65E5 +// 29 | [$-411]ggge"5E74"m"6708"d"65E5 +// 30 | m/d/y +// 31 | yyyy"5E74"m"6708"d"65E5 +// 32 | h"6642"mm"5206 +// 33 | h"6642"mm"5206"ss"79D2 +// 34 | yyyy"5E74"m"6708 +// 35 | m"6708"d"65E5 +// 36 | [$-411]ge.m.d +// 50 | [$-411]ge.m.d +// 51 | [$-411]ggge"5E74"m"6708"d"65E5 +// 52 | yyyy"5E74"m"6708 +// 53 | m"6708"d"65E5 +// 54 | [$-411]ggge"5E74"m"6708"d"65E5 +// 55 | yyyy"5E74"m"6708 +// 56 | m"6708"d"65E5 +// 57 | [$-411]ge.m.d +// 58 | [$-411]ggge"5E74"m"6708"d"65E5" // // Number format code with unicode values provided for language glyphs where // they occur in ko-kr language: // -// Index | Symbol -// -------+------------------------------------------- -// 27 | yyyy"5E74" mm"6708" dd"65E5 -// 28 | mm-d -// 29 | mm-d -// 30 | mm-dd-y -// 31 | yyyy"B144" mm"C6D4" dd"C77C -// 32 | h"C2DC" mm"BD84 -// 33 | h"C2DC" mm"BD84" ss"CD08 -// 34 | yyyy-mm-d -// 35 | yyyy-mm-d -// 36 | yyyy"5E74" mm"6708" dd"65E5 -// 50 | yyyy"5E74" mm"6708" dd"65E5 -// 51 | mm-d -// 52 | yyyy-mm-d -// 53 | yyyy-mm-d -// 54 | mm-d -// 55 | yyyy-mm-d -// 56 | yyyy-mm-d -// 57 | yyyy"5E74" mm"6708" dd"65E5 -// 58 | mm-dd +// Index | Symbol +// -------+------------------------------------------- +// 27 | yyyy"5E74" mm"6708" dd"65E5 +// 28 | mm-d +// 29 | mm-d +// 30 | mm-dd-y +// 31 | yyyy"B144" mm"C6D4" dd"C77C +// 32 | h"C2DC" mm"BD84 +// 33 | h"C2DC" mm"BD84" ss"CD08 +// 34 | yyyy-mm-d +// 35 | yyyy-mm-d +// 36 | yyyy"5E74" mm"6708" dd"65E5 +// 50 | yyyy"5E74" mm"6708" dd"65E5 +// 51 | mm-d +// 52 | yyyy-mm-d +// 53 | yyyy-mm-d +// 54 | mm-d +// 55 | yyyy-mm-d +// 56 | yyyy-mm-d +// 57 | yyyy"5E74" mm"6708" dd"65E5 +// 58 | mm-dd // // Number format code in th-th language: // -// Index | Symbol -// -------+------------------------------------------- -// 59 | t -// 60 | t0.0 -// 61 | t#,## -// 62 | t#,##0.0 -// 67 | t0 -// 68 | t0.00 -// 69 | t# ?/ -// 70 | t# ??/? -// 71 | ว/ด/ปปป -// 72 | ว-ดดด-ป -// 73 | ว-ดด -// 74 | ดดด-ป -// 75 | ช:น -// 76 | ช:นน:ท -// 77 | ว/ด/ปปปป ช:น -// 78 | นน:ท -// 79 | [ช]:นน:ท -// 80 | นน:ทท. -// 81 | d/m/bb +// Index | Symbol +// -------+------------------------------------------- +// 59 | t +// 60 | t0.0 +// 61 | t#,## +// 62 | t#,##0.0 +// 67 | t0 +// 68 | t0.00 +// 69 | t# ?/ +// 70 | t# ??/? +// 71 | ว/ด/ปปป +// 72 | ว-ดดด-ป +// 73 | ว-ดด +// 74 | ดดด-ป +// 75 | ช:น +// 76 | ช:นน:ท +// 77 | ว/ด/ปปปป ช:น +// 78 | นน:ท +// 79 | [ช]:นน:ท +// 80 | นน:ทท. +// 81 | d/m/bb // // Number format code with unicode values provided for language glyphs where // they occur in th-th language: // -// Index | Symbol -// -------+------------------------------------------- -// 59 | t -// 60 | t0.0 -// 61 | t#,## -// 62 | t#,##0.0 -// 67 | t0 -// 68 | t0.00 -// 69 | t# ?/ -// 70 | t# ??/? -// 71 | 0E27/0E14/0E1B0E1B0E1B0E1 -// 72 | 0E27-0E140E140E14-0E1B0E1 -// 73 | 0E27-0E140E140E1 -// 74 | 0E140E140E14-0E1B0E1 -// 75 | 0E0A:0E190E1 -// 76 | 0E0A:0E190E19:0E170E1 -// 77 | 0E27/0E14/0E1B0E1B0E1B0E1B 0E0A:0E190E1 -// 78 | 0E190E19:0E170E1 -// 79 | [0E0A]:0E190E19:0E170E1 -// 80 | 0E190E19:0E170E17. -// 81 | d/m/bb +// Index | Symbol +// -------+------------------------------------------- +// 59 | t +// 60 | t0.0 +// 61 | t#,## +// 62 | t#,##0.0 +// 67 | t0 +// 68 | t0.00 +// 69 | t# ?/ +// 70 | t# ??/? +// 71 | 0E27/0E14/0E1B0E1B0E1B0E1 +// 72 | 0E27-0E140E140E14-0E1B0E1 +// 73 | 0E27-0E140E140E1 +// 74 | 0E140E140E14-0E1B0E1 +// 75 | 0E0A:0E190E1 +// 76 | 0E0A:0E190E19:0E170E1 +// 77 | 0E27/0E14/0E1B0E1B0E1B0E1B 0E0A:0E190E1 +// 78 | 0E190E19:0E170E1 +// 79 | [0E0A]:0E190E19:0E170E1 +// 80 | 0E190E19:0E170E17. +// 81 | d/m/bb // // Excelize built-in currency formats are shown in the following table, only // support these types in the following table (Index number is used only for // markup and is not used inside an Excel file and you can't get formatted value // by the function GetCellValue) currently: // -// Index | Symbol -// -------+--------------------------------------------------------------- -// 164 | CN¥ -// 165 | $ English (China) -// 166 | $ Cherokee (United States) -// 167 | $ Chinese (Singapore) -// 168 | $ Chinese (Taiwan) -// 169 | $ English (Australia) -// 170 | $ English (Belize) -// 171 | $ English (Canada) -// 172 | $ English (Jamaica) -// 173 | $ English (New Zealand) -// 174 | $ English (Singapore) -// 175 | $ English (Trinidad & Tobago) -// 176 | $ English (U.S. Virgin Islands) -// 177 | $ English (United States) -// 178 | $ French (Canada) -// 179 | $ Hawaiian (United States) -// 180 | $ Malay (Brunei) -// 181 | $ Quechua (Ecuador) -// 182 | $ Spanish (Chile) -// 183 | $ Spanish (Colombia) -// 184 | $ Spanish (Ecuador) -// 185 | $ Spanish (El Salvador) -// 186 | $ Spanish (Mexico) -// 187 | $ Spanish (Puerto Rico) -// 188 | $ Spanish (United States) -// 189 | $ Spanish (Uruguay) -// 190 | £ English (United Kingdom) -// 191 | £ Scottish Gaelic (United Kingdom) -// 192 | £ Welsh (United Kindom) -// 193 | ¥ Chinese (China) -// 194 | ¥ Japanese (Japan) -// 195 | ¥ Sichuan Yi (China) -// 196 | ¥ Tibetan (China) -// 197 | ¥ Uyghur (China) -// 198 | ֏ Armenian (Armenia) -// 199 | ؋ Pashto (Afghanistan) -// 200 | ؋ Persian (Afghanistan) -// 201 | ৳ Bengali (Bangladesh) -// 202 | ៛ Khmer (Cambodia) -// 203 | ₡ Spanish (Costa Rica) -// 204 | ₦ Hausa (Nigeria) -// 205 | ₦ Igbo (Nigeria) -// 206 | ₦ Yoruba (Nigeria) -// 207 | ₩ Korean (South Korea) -// 208 | ₪ Hebrew (Israel) -// 209 | ₫ Vietnamese (Vietnam) -// 210 | € Basque (Spain) -// 211 | € Breton (France) -// 212 | € Catalan (Spain) -// 213 | € Corsican (France) -// 214 | € Dutch (Belgium) -// 215 | € Dutch (Netherlands) -// 216 | € English (Ireland) -// 217 | € Estonian (Estonia) -// 218 | € Euro (€ 123) -// 219 | € Euro (123 €) -// 220 | € Finnish (Finland) -// 221 | € French (Belgium) -// 222 | € French (France) -// 223 | € French (Luxembourg) -// 224 | € French (Monaco) -// 225 | € French (Réunion) -// 226 | € Galician (Spain) -// 227 | € German (Austria) -// 228 | € German (Luxembourg) -// 229 | € Greek (Greece) -// 230 | € Inari Sami (Finland) -// 231 | € Irish (Ireland) -// 232 | € Italian (Italy) -// 233 | € Latin (Italy) -// 234 | € Latin, Serbian (Montenegro) -// 235 | € Larvian (Latvia) -// 236 | € Lithuanian (Lithuania) -// 237 | € Lower Sorbian (Germany) -// 238 | € Luxembourgish (Luxembourg) -// 239 | € Maltese (Malta) -// 240 | € Northern Sami (Finland) -// 241 | € Occitan (France) -// 242 | € Portuguese (Portugal) -// 243 | € Serbian (Montenegro) -// 244 | € Skolt Sami (Finland) -// 245 | € Slovak (Slovakia) -// 246 | € Slovenian (Slovenia) -// 247 | € Spanish (Spain) -// 248 | € Swedish (Finland) -// 249 | € Swiss German (France) -// 250 | € Upper Sorbian (Germany) -// 251 | € Western Frisian (Netherlands) -// 252 | ₭ Lao (Laos) -// 253 | ₮ Mongolian (Mongolia) -// 254 | ₮ Mongolian, Mongolian (Mongolia) -// 255 | ₱ English (Philippines) -// 256 | ₱ Filipino (Philippines) -// 257 | ₴ Ukrainian (Ukraine) -// 258 | ₸ Kazakh (Kazakhstan) -// 259 | ₹ Arabic, Kashmiri (India) -// 260 | ₹ English (India) -// 261 | ₹ Gujarati (India) -// 262 | ₹ Hindi (India) -// 263 | ₹ Kannada (India) -// 264 | ₹ Kashmiri (India) -// 265 | ₹ Konkani (India) -// 266 | ₹ Manipuri (India) -// 267 | ₹ Marathi (India) -// 268 | ₹ Nepali (India) -// 269 | ₹ Oriya (India) -// 270 | ₹ Punjabi (India) -// 271 | ₹ Sanskrit (India) -// 272 | ₹ Sindhi (India) -// 273 | ₹ Tamil (India) -// 274 | ₹ Urdu (India) -// 275 | ₺ Turkish (Turkey) -// 276 | ₼ Azerbaijani (Azerbaijan) -// 277 | ₼ Cyrillic, Azerbaijani (Azerbaijan) -// 278 | ₽ Russian (Russia) -// 279 | ₽ Sakha (Russia) -// 280 | ₾ Georgian (Georgia) -// 281 | B/. Spanish (Panama) -// 282 | Br Oromo (Ethiopia) -// 283 | Br Somali (Ethiopia) -// 284 | Br Tigrinya (Ethiopia) -// 285 | Bs Quechua (Bolivia) -// 286 | Bs Spanish (Bolivia) -// 287 | BS. Spanish (Venezuela) -// 288 | BWP Tswana (Botswana) -// 289 | C$ Spanish (Nicaragua) -// 290 | CA$ Latin, Inuktitut (Canada) -// 291 | CA$ Mohawk (Canada) -// 292 | CA$ Unified Canadian Aboriginal Syllabics, Inuktitut (Canada) -// 293 | CFA French (Mali) -// 294 | CFA French (Senegal) -// 295 | CFA Fulah (Senegal) -// 296 | CFA Wolof (Senegal) -// 297 | CHF French (Switzerland) -// 298 | CHF German (Liechtenstein) -// 299 | CHF German (Switzerland) -// 300 | CHF Italian (Switzerland) -// 301 | CHF Romansh (Switzerland) -// 302 | CLP Mapuche (Chile) -// 303 | CN¥ Mongolian, Mongolian (China) -// 304 | DZD Central Atlas Tamazight (Algeria) -// 305 | FCFA French (Cameroon) -// 306 | Ft Hungarian (Hungary) -// 307 | G French (Haiti) -// 308 | Gs. Spanish (Paraguay) -// 309 | GTQ K'iche' (Guatemala) -// 310 | HK$ Chinese (Hong Kong (China)) -// 311 | HK$ English (Hong Kong (China)) -// 312 | HRK Croatian (Croatia) -// 313 | IDR English (Indonesia) -// 314 | IQD Arbic, Central Kurdish (Iraq) -// 315 | ISK Icelandic (Iceland) -// 316 | K Burmese (Myanmar (Burma)) -// 317 | Kč Czech (Czech Republic) -// 318 | KM Bosnian (Bosnia & Herzegovina) -// 319 | KM Croatian (Bosnia & Herzegovina) -// 320 | KM Latin, Serbian (Bosnia & Herzegovina) -// 321 | kr Faroese (Faroe Islands) -// 322 | kr Northern Sami (Norway) -// 323 | kr Northern Sami (Sweden) -// 324 | kr Norwegian Bokmål (Norway) -// 325 | kr Norwegian Nynorsk (Norway) -// 326 | kr Swedish (Sweden) -// 327 | kr. Danish (Denmark) -// 328 | kr. Kalaallisut (Greenland) -// 329 | Ksh Swahili (kenya) -// 330 | L Romanian (Moldova) -// 331 | L Russian (Moldova) -// 332 | L Spanish (Honduras) -// 333 | Lekë Albanian (Albania) -// 334 | MAD Arabic, Central Atlas Tamazight (Morocco) -// 335 | MAD French (Morocco) -// 336 | MAD Tifinagh, Central Atlas Tamazight (Morocco) -// 337 | MOP$ Chinese (Macau (China)) -// 338 | MVR Divehi (Maldives) -// 339 | Nfk Tigrinya (Eritrea) -// 340 | NGN Bini (Nigeria) -// 341 | NGN Fulah (Nigeria) -// 342 | NGN Ibibio (Nigeria) -// 343 | NGN Kanuri (Nigeria) -// 344 | NOK Lule Sami (Norway) -// 345 | NOK Southern Sami (Norway) -// 346 | NZ$ Maori (New Zealand) -// 347 | PKR Sindhi (Pakistan) -// 348 | PYG Guarani (Paraguay) -// 349 | Q Spanish (Guatemala) -// 350 | R Afrikaans (South Africa) -// 351 | R English (South Africa) -// 352 | R Zulu (South Africa) -// 353 | R$ Portuguese (Brazil) -// 354 | RD$ Spanish (Dominican Republic) -// 355 | RF Kinyarwanda (Rwanda) -// 356 | RM English (Malaysia) -// 357 | RM Malay (Malaysia) -// 358 | RON Romanian (Romania) -// 359 | Rp Indonesoan (Indonesia) -// 360 | Rs Urdu (Pakistan) -// 361 | Rs. Tamil (Sri Lanka) -// 362 | RSD Latin, Serbian (Serbia) -// 363 | RSD Serbian (Serbia) -// 364 | RUB Bashkir (Russia) -// 365 | RUB Tatar (Russia) -// 366 | S/. Quechua (Peru) -// 367 | S/. Spanish (Peru) -// 368 | SEK Lule Sami (Sweden) -// 369 | SEK Southern Sami (Sweden) -// 370 | soʻm Latin, Uzbek (Uzbekistan) -// 371 | soʻm Uzbek (Uzbekistan) -// 372 | SYP Syriac (Syria) -// 373 | THB Thai (Thailand) -// 374 | TMT Turkmen (Turkmenistan) -// 375 | US$ English (Zimbabwe) -// 376 | ZAR Northern Sotho (South Africa) -// 377 | ZAR Southern Sotho (South Africa) -// 378 | ZAR Tsonga (South Africa) -// 379 | ZAR Tswana (south Africa) -// 380 | ZAR Venda (South Africa) -// 381 | ZAR Xhosa (South Africa) -// 382 | zł Polish (Poland) -// 383 | ден Macedonian (Macedonia) -// 384 | KM Cyrillic, Bosnian (Bosnia & Herzegovina) -// 385 | KM Serbian (Bosnia & Herzegovina) -// 386 | лв. Bulgarian (Bulgaria) -// 387 | p. Belarusian (Belarus) -// 388 | сом Kyrgyz (Kyrgyzstan) -// 389 | сом Tajik (Tajikistan) -// 390 | ج.م. Arabic (Egypt) -// 391 | د.أ. Arabic (Jordan) -// 392 | د.أ. Arabic (United Arab Emirates) -// 393 | د.ب. Arabic (Bahrain) -// 394 | د.ت. Arabic (Tunisia) -// 395 | د.ج. Arabic (Algeria) -// 396 | د.ع. Arabic (Iraq) -// 397 | د.ك. Arabic (Kuwait) -// 398 | د.ل. Arabic (Libya) -// 399 | د.م. Arabic (Morocco) -// 400 | ر Punjabi (Pakistan) -// 401 | ر.س. Arabic (Saudi Arabia) -// 402 | ر.ع. Arabic (Oman) -// 403 | ر.ق. Arabic (Qatar) -// 404 | ر.ي. Arabic (Yemen) -// 405 | ریال Persian (Iran) -// 406 | ل.س. Arabic (Syria) -// 407 | ل.ل. Arabic (Lebanon) -// 408 | ብር Amharic (Ethiopia) -// 409 | रू Nepaol (Nepal) -// 410 | රු. Sinhala (Sri Lanka) -// 411 | ADP -// 412 | AED -// 413 | AFA -// 414 | AFN -// 415 | ALL -// 416 | AMD -// 417 | ANG -// 418 | AOA -// 419 | ARS -// 420 | ATS -// 421 | AUD -// 422 | AWG -// 423 | AZM -// 424 | AZN -// 425 | BAM -// 426 | BBD -// 427 | BDT -// 428 | BEF -// 429 | BGL -// 430 | BGN -// 431 | BHD -// 432 | BIF -// 433 | BMD -// 434 | BND -// 435 | BOB -// 436 | BOV -// 437 | BRL -// 438 | BSD -// 439 | BTN -// 440 | BWP -// 441 | BYR -// 442 | BZD -// 443 | CAD -// 444 | CDF -// 445 | CHE -// 446 | CHF -// 447 | CHW -// 448 | CLF -// 449 | CLP -// 450 | CNY -// 451 | COP -// 452 | COU -// 453 | CRC -// 454 | CSD -// 455 | CUC -// 456 | CVE -// 457 | CYP -// 458 | CZK -// 459 | DEM -// 460 | DJF -// 461 | DKK -// 462 | DOP -// 463 | DZD -// 464 | ECS -// 465 | ECV -// 466 | EEK -// 467 | EGP -// 468 | ERN -// 469 | ESP -// 470 | ETB -// 471 | EUR -// 472 | FIM -// 473 | FJD -// 474 | FKP -// 475 | FRF -// 476 | GBP -// 477 | GEL -// 478 | GHC -// 479 | GHS -// 480 | GIP -// 481 | GMD -// 482 | GNF -// 483 | GRD -// 484 | GTQ -// 485 | GYD -// 486 | HKD -// 487 | HNL -// 488 | HRK -// 489 | HTG -// 490 | HUF -// 491 | IDR -// 492 | IEP -// 493 | ILS -// 494 | INR -// 495 | IQD -// 496 | IRR -// 497 | ISK -// 498 | ITL -// 499 | JMD -// 500 | JOD -// 501 | JPY -// 502 | KAF -// 503 | KES -// 504 | KGS -// 505 | KHR -// 506 | KMF -// 507 | KPW -// 508 | KRW -// 509 | KWD -// 510 | KYD -// 511 | KZT -// 512 | LAK -// 513 | LBP -// 514 | LKR -// 515 | LRD -// 516 | LSL -// 517 | LTL -// 518 | LUF -// 519 | LVL -// 520 | LYD -// 521 | MAD -// 522 | MDL -// 523 | MGA -// 524 | MGF -// 525 | MKD -// 526 | MMK -// 527 | MNT -// 528 | MOP -// 529 | MRO -// 530 | MTL -// 531 | MUR -// 532 | MVR -// 533 | MWK -// 534 | MXN -// 535 | MXV -// 536 | MYR -// 537 | MZM -// 538 | MZN -// 539 | NAD -// 540 | NGN -// 541 | NIO -// 542 | NLG -// 543 | NOK -// 544 | NPR -// 545 | NTD -// 546 | NZD -// 547 | OMR -// 548 | PAB -// 549 | PEN -// 550 | PGK -// 551 | PHP -// 552 | PKR -// 553 | PLN -// 554 | PTE -// 555 | PYG -// 556 | QAR -// 557 | ROL -// 558 | RON -// 559 | RSD -// 560 | RUB -// 561 | RUR -// 562 | RWF -// 563 | SAR -// 564 | SBD -// 565 | SCR -// 566 | SDD -// 567 | SDG -// 568 | SDP -// 569 | SEK -// 570 | SGD -// 571 | SHP -// 572 | SIT -// 573 | SKK -// 574 | SLL -// 575 | SOS -// 576 | SPL -// 577 | SRD -// 578 | SRG -// 579 | STD -// 580 | SVC -// 581 | SYP -// 582 | SZL -// 583 | THB -// 584 | TJR -// 585 | TJS -// 586 | TMM -// 587 | TMT -// 588 | TND -// 589 | TOP -// 590 | TRL -// 591 | TRY -// 592 | TTD -// 593 | TWD -// 594 | TZS -// 595 | UAH -// 596 | UGX -// 597 | USD -// 598 | USN -// 599 | USS -// 600 | UYI -// 601 | UYU -// 602 | UZS -// 603 | VEB -// 604 | VEF -// 605 | VND -// 606 | VUV -// 607 | WST -// 608 | XAF -// 609 | XAG -// 610 | XAU -// 611 | XB5 -// 612 | XBA -// 613 | XBB -// 614 | XBC -// 615 | XBD -// 616 | XCD -// 617 | XDR -// 618 | XFO -// 619 | XFU -// 620 | XOF -// 621 | XPD -// 622 | XPF -// 623 | XPT -// 624 | XTS -// 625 | XXX -// 626 | YER -// 627 | YUM -// 628 | ZAR -// 629 | ZMK -// 630 | ZMW -// 631 | ZWD -// 632 | ZWL -// 633 | ZWN -// 634 | ZWR +// Index | Symbol +// -------+--------------------------------------------------------------- +// 164 | CN¥ +// 165 | $ English (China) +// 166 | $ Cherokee (United States) +// 167 | $ Chinese (Singapore) +// 168 | $ Chinese (Taiwan) +// 169 | $ English (Australia) +// 170 | $ English (Belize) +// 171 | $ English (Canada) +// 172 | $ English (Jamaica) +// 173 | $ English (New Zealand) +// 174 | $ English (Singapore) +// 175 | $ English (Trinidad & Tobago) +// 176 | $ English (U.S. Virgin Islands) +// 177 | $ English (United States) +// 178 | $ French (Canada) +// 179 | $ Hawaiian (United States) +// 180 | $ Malay (Brunei) +// 181 | $ Quechua (Ecuador) +// 182 | $ Spanish (Chile) +// 183 | $ Spanish (Colombia) +// 184 | $ Spanish (Ecuador) +// 185 | $ Spanish (El Salvador) +// 186 | $ Spanish (Mexico) +// 187 | $ Spanish (Puerto Rico) +// 188 | $ Spanish (United States) +// 189 | $ Spanish (Uruguay) +// 190 | £ English (United Kingdom) +// 191 | £ Scottish Gaelic (United Kingdom) +// 192 | £ Welsh (United Kindom) +// 193 | ¥ Chinese (China) +// 194 | ¥ Japanese (Japan) +// 195 | ¥ Sichuan Yi (China) +// 196 | ¥ Tibetan (China) +// 197 | ¥ Uyghur (China) +// 198 | ֏ Armenian (Armenia) +// 199 | ؋ Pashto (Afghanistan) +// 200 | ؋ Persian (Afghanistan) +// 201 | ৳ Bengali (Bangladesh) +// 202 | ៛ Khmer (Cambodia) +// 203 | ₡ Spanish (Costa Rica) +// 204 | ₦ Hausa (Nigeria) +// 205 | ₦ Igbo (Nigeria) +// 206 | ₦ Yoruba (Nigeria) +// 207 | ₩ Korean (South Korea) +// 208 | ₪ Hebrew (Israel) +// 209 | ₫ Vietnamese (Vietnam) +// 210 | € Basque (Spain) +// 211 | € Breton (France) +// 212 | € Catalan (Spain) +// 213 | € Corsican (France) +// 214 | € Dutch (Belgium) +// 215 | € Dutch (Netherlands) +// 216 | € English (Ireland) +// 217 | € Estonian (Estonia) +// 218 | € Euro (€ 123) +// 219 | € Euro (123 €) +// 220 | € Finnish (Finland) +// 221 | € French (Belgium) +// 222 | € French (France) +// 223 | € French (Luxembourg) +// 224 | € French (Monaco) +// 225 | € French (Réunion) +// 226 | € Galician (Spain) +// 227 | € German (Austria) +// 228 | € German (Luxembourg) +// 229 | € Greek (Greece) +// 230 | € Inari Sami (Finland) +// 231 | € Irish (Ireland) +// 232 | € Italian (Italy) +// 233 | € Latin (Italy) +// 234 | € Latin, Serbian (Montenegro) +// 235 | € Larvian (Latvia) +// 236 | € Lithuanian (Lithuania) +// 237 | € Lower Sorbian (Germany) +// 238 | € Luxembourgish (Luxembourg) +// 239 | € Maltese (Malta) +// 240 | € Northern Sami (Finland) +// 241 | € Occitan (France) +// 242 | € Portuguese (Portugal) +// 243 | € Serbian (Montenegro) +// 244 | € Skolt Sami (Finland) +// 245 | € Slovak (Slovakia) +// 246 | € Slovenian (Slovenia) +// 247 | € Spanish (Spain) +// 248 | € Swedish (Finland) +// 249 | € Swiss German (France) +// 250 | € Upper Sorbian (Germany) +// 251 | € Western Frisian (Netherlands) +// 252 | ₭ Lao (Laos) +// 253 | ₮ Mongolian (Mongolia) +// 254 | ₮ Mongolian, Mongolian (Mongolia) +// 255 | ₱ English (Philippines) +// 256 | ₱ Filipino (Philippines) +// 257 | ₴ Ukrainian (Ukraine) +// 258 | ₸ Kazakh (Kazakhstan) +// 259 | ₹ Arabic, Kashmiri (India) +// 260 | ₹ English (India) +// 261 | ₹ Gujarati (India) +// 262 | ₹ Hindi (India) +// 263 | ₹ Kannada (India) +// 264 | ₹ Kashmiri (India) +// 265 | ₹ Konkani (India) +// 266 | ₹ Manipuri (India) +// 267 | ₹ Marathi (India) +// 268 | ₹ Nepali (India) +// 269 | ₹ Oriya (India) +// 270 | ₹ Punjabi (India) +// 271 | ₹ Sanskrit (India) +// 272 | ₹ Sindhi (India) +// 273 | ₹ Tamil (India) +// 274 | ₹ Urdu (India) +// 275 | ₺ Turkish (Turkey) +// 276 | ₼ Azerbaijani (Azerbaijan) +// 277 | ₼ Cyrillic, Azerbaijani (Azerbaijan) +// 278 | ₽ Russian (Russia) +// 279 | ₽ Sakha (Russia) +// 280 | ₾ Georgian (Georgia) +// 281 | B/. Spanish (Panama) +// 282 | Br Oromo (Ethiopia) +// 283 | Br Somali (Ethiopia) +// 284 | Br Tigrinya (Ethiopia) +// 285 | Bs Quechua (Bolivia) +// 286 | Bs Spanish (Bolivia) +// 287 | BS. Spanish (Venezuela) +// 288 | BWP Tswana (Botswana) +// 289 | C$ Spanish (Nicaragua) +// 290 | CA$ Latin, Inuktitut (Canada) +// 291 | CA$ Mohawk (Canada) +// 292 | CA$ Unified Canadian Aboriginal Syllabics, Inuktitut (Canada) +// 293 | CFA French (Mali) +// 294 | CFA French (Senegal) +// 295 | CFA Fulah (Senegal) +// 296 | CFA Wolof (Senegal) +// 297 | CHF French (Switzerland) +// 298 | CHF German (Liechtenstein) +// 299 | CHF German (Switzerland) +// 300 | CHF Italian (Switzerland) +// 301 | CHF Romansh (Switzerland) +// 302 | CLP Mapuche (Chile) +// 303 | CN¥ Mongolian, Mongolian (China) +// 304 | DZD Central Atlas Tamazight (Algeria) +// 305 | FCFA French (Cameroon) +// 306 | Ft Hungarian (Hungary) +// 307 | G French (Haiti) +// 308 | Gs. Spanish (Paraguay) +// 309 | GTQ K'iche' (Guatemala) +// 310 | HK$ Chinese (Hong Kong (China)) +// 311 | HK$ English (Hong Kong (China)) +// 312 | HRK Croatian (Croatia) +// 313 | IDR English (Indonesia) +// 314 | IQD Arbic, Central Kurdish (Iraq) +// 315 | ISK Icelandic (Iceland) +// 316 | K Burmese (Myanmar (Burma)) +// 317 | Kč Czech (Czech Republic) +// 318 | KM Bosnian (Bosnia & Herzegovina) +// 319 | KM Croatian (Bosnia & Herzegovina) +// 320 | KM Latin, Serbian (Bosnia & Herzegovina) +// 321 | kr Faroese (Faroe Islands) +// 322 | kr Northern Sami (Norway) +// 323 | kr Northern Sami (Sweden) +// 324 | kr Norwegian Bokmål (Norway) +// 325 | kr Norwegian Nynorsk (Norway) +// 326 | kr Swedish (Sweden) +// 327 | kr. Danish (Denmark) +// 328 | kr. Kalaallisut (Greenland) +// 329 | Ksh Swahili (kenya) +// 330 | L Romanian (Moldova) +// 331 | L Russian (Moldova) +// 332 | L Spanish (Honduras) +// 333 | Lekë Albanian (Albania) +// 334 | MAD Arabic, Central Atlas Tamazight (Morocco) +// 335 | MAD French (Morocco) +// 336 | MAD Tifinagh, Central Atlas Tamazight (Morocco) +// 337 | MOP$ Chinese (Macau (China)) +// 338 | MVR Divehi (Maldives) +// 339 | Nfk Tigrinya (Eritrea) +// 340 | NGN Bini (Nigeria) +// 341 | NGN Fulah (Nigeria) +// 342 | NGN Ibibio (Nigeria) +// 343 | NGN Kanuri (Nigeria) +// 344 | NOK Lule Sami (Norway) +// 345 | NOK Southern Sami (Norway) +// 346 | NZ$ Maori (New Zealand) +// 347 | PKR Sindhi (Pakistan) +// 348 | PYG Guarani (Paraguay) +// 349 | Q Spanish (Guatemala) +// 350 | R Afrikaans (South Africa) +// 351 | R English (South Africa) +// 352 | R Zulu (South Africa) +// 353 | R$ Portuguese (Brazil) +// 354 | RD$ Spanish (Dominican Republic) +// 355 | RF Kinyarwanda (Rwanda) +// 356 | RM English (Malaysia) +// 357 | RM Malay (Malaysia) +// 358 | RON Romanian (Romania) +// 359 | Rp Indonesoan (Indonesia) +// 360 | Rs Urdu (Pakistan) +// 361 | Rs. Tamil (Sri Lanka) +// 362 | RSD Latin, Serbian (Serbia) +// 363 | RSD Serbian (Serbia) +// 364 | RUB Bashkir (Russia) +// 365 | RUB Tatar (Russia) +// 366 | S/. Quechua (Peru) +// 367 | S/. Spanish (Peru) +// 368 | SEK Lule Sami (Sweden) +// 369 | SEK Southern Sami (Sweden) +// 370 | soʻm Latin, Uzbek (Uzbekistan) +// 371 | soʻm Uzbek (Uzbekistan) +// 372 | SYP Syriac (Syria) +// 373 | THB Thai (Thailand) +// 374 | TMT Turkmen (Turkmenistan) +// 375 | US$ English (Zimbabwe) +// 376 | ZAR Northern Sotho (South Africa) +// 377 | ZAR Southern Sotho (South Africa) +// 378 | ZAR Tsonga (South Africa) +// 379 | ZAR Tswana (south Africa) +// 380 | ZAR Venda (South Africa) +// 381 | ZAR Xhosa (South Africa) +// 382 | zł Polish (Poland) +// 383 | ден Macedonian (Macedonia) +// 384 | KM Cyrillic, Bosnian (Bosnia & Herzegovina) +// 385 | KM Serbian (Bosnia & Herzegovina) +// 386 | лв. Bulgarian (Bulgaria) +// 387 | p. Belarusian (Belarus) +// 388 | сом Kyrgyz (Kyrgyzstan) +// 389 | сом Tajik (Tajikistan) +// 390 | ج.م. Arabic (Egypt) +// 391 | د.أ. Arabic (Jordan) +// 392 | د.أ. Arabic (United Arab Emirates) +// 393 | د.ب. Arabic (Bahrain) +// 394 | د.ت. Arabic (Tunisia) +// 395 | د.ج. Arabic (Algeria) +// 396 | د.ع. Arabic (Iraq) +// 397 | د.ك. Arabic (Kuwait) +// 398 | د.ل. Arabic (Libya) +// 399 | د.م. Arabic (Morocco) +// 400 | ر Punjabi (Pakistan) +// 401 | ر.س. Arabic (Saudi Arabia) +// 402 | ر.ع. Arabic (Oman) +// 403 | ر.ق. Arabic (Qatar) +// 404 | ر.ي. Arabic (Yemen) +// 405 | ریال Persian (Iran) +// 406 | ل.س. Arabic (Syria) +// 407 | ل.ل. Arabic (Lebanon) +// 408 | ብር Amharic (Ethiopia) +// 409 | रू Nepaol (Nepal) +// 410 | රු. Sinhala (Sri Lanka) +// 411 | ADP +// 412 | AED +// 413 | AFA +// 414 | AFN +// 415 | ALL +// 416 | AMD +// 417 | ANG +// 418 | AOA +// 419 | ARS +// 420 | ATS +// 421 | AUD +// 422 | AWG +// 423 | AZM +// 424 | AZN +// 425 | BAM +// 426 | BBD +// 427 | BDT +// 428 | BEF +// 429 | BGL +// 430 | BGN +// 431 | BHD +// 432 | BIF +// 433 | BMD +// 434 | BND +// 435 | BOB +// 436 | BOV +// 437 | BRL +// 438 | BSD +// 439 | BTN +// 440 | BWP +// 441 | BYR +// 442 | BZD +// 443 | CAD +// 444 | CDF +// 445 | CHE +// 446 | CHF +// 447 | CHW +// 448 | CLF +// 449 | CLP +// 450 | CNY +// 451 | COP +// 452 | COU +// 453 | CRC +// 454 | CSD +// 455 | CUC +// 456 | CVE +// 457 | CYP +// 458 | CZK +// 459 | DEM +// 460 | DJF +// 461 | DKK +// 462 | DOP +// 463 | DZD +// 464 | ECS +// 465 | ECV +// 466 | EEK +// 467 | EGP +// 468 | ERN +// 469 | ESP +// 470 | ETB +// 471 | EUR +// 472 | FIM +// 473 | FJD +// 474 | FKP +// 475 | FRF +// 476 | GBP +// 477 | GEL +// 478 | GHC +// 479 | GHS +// 480 | GIP +// 481 | GMD +// 482 | GNF +// 483 | GRD +// 484 | GTQ +// 485 | GYD +// 486 | HKD +// 487 | HNL +// 488 | HRK +// 489 | HTG +// 490 | HUF +// 491 | IDR +// 492 | IEP +// 493 | ILS +// 494 | INR +// 495 | IQD +// 496 | IRR +// 497 | ISK +// 498 | ITL +// 499 | JMD +// 500 | JOD +// 501 | JPY +// 502 | KAF +// 503 | KES +// 504 | KGS +// 505 | KHR +// 506 | KMF +// 507 | KPW +// 508 | KRW +// 509 | KWD +// 510 | KYD +// 511 | KZT +// 512 | LAK +// 513 | LBP +// 514 | LKR +// 515 | LRD +// 516 | LSL +// 517 | LTL +// 518 | LUF +// 519 | LVL +// 520 | LYD +// 521 | MAD +// 522 | MDL +// 523 | MGA +// 524 | MGF +// 525 | MKD +// 526 | MMK +// 527 | MNT +// 528 | MOP +// 529 | MRO +// 530 | MTL +// 531 | MUR +// 532 | MVR +// 533 | MWK +// 534 | MXN +// 535 | MXV +// 536 | MYR +// 537 | MZM +// 538 | MZN +// 539 | NAD +// 540 | NGN +// 541 | NIO +// 542 | NLG +// 543 | NOK +// 544 | NPR +// 545 | NTD +// 546 | NZD +// 547 | OMR +// 548 | PAB +// 549 | PEN +// 550 | PGK +// 551 | PHP +// 552 | PKR +// 553 | PLN +// 554 | PTE +// 555 | PYG +// 556 | QAR +// 557 | ROL +// 558 | RON +// 559 | RSD +// 560 | RUB +// 561 | RUR +// 562 | RWF +// 563 | SAR +// 564 | SBD +// 565 | SCR +// 566 | SDD +// 567 | SDG +// 568 | SDP +// 569 | SEK +// 570 | SGD +// 571 | SHP +// 572 | SIT +// 573 | SKK +// 574 | SLL +// 575 | SOS +// 576 | SPL +// 577 | SRD +// 578 | SRG +// 579 | STD +// 580 | SVC +// 581 | SYP +// 582 | SZL +// 583 | THB +// 584 | TJR +// 585 | TJS +// 586 | TMM +// 587 | TMT +// 588 | TND +// 589 | TOP +// 590 | TRL +// 591 | TRY +// 592 | TTD +// 593 | TWD +// 594 | TZS +// 595 | UAH +// 596 | UGX +// 597 | USD +// 598 | USN +// 599 | USS +// 600 | UYI +// 601 | UYU +// 602 | UZS +// 603 | VEB +// 604 | VEF +// 605 | VND +// 606 | VUV +// 607 | WST +// 608 | XAF +// 609 | XAG +// 610 | XAU +// 611 | XB5 +// 612 | XBA +// 613 | XBB +// 614 | XBC +// 615 | XBD +// 616 | XCD +// 617 | XDR +// 618 | XFO +// 619 | XFU +// 620 | XOF +// 621 | XPD +// 622 | XPF +// 623 | XPT +// 624 | XTS +// 625 | XXX +// 626 | YER +// 627 | YUM +// 628 | ZAR +// 629 | ZMK +// 630 | ZMW +// 631 | ZWD +// 632 | ZWL +// 633 | ZWN +// 634 | ZWR // // Excelize support set custom number format for cell. For example, set number // as date type in Uruguay (Spanish) format for Sheet1!A6: // -// f := excelize.NewFile() -// f.SetCellValue("Sheet1", "A6", 42920.5) -// exp := "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@" -// style, err := f.NewStyle(&excelize.Style{CustomNumFmt: &exp}) -// err = f.SetCellStyle("Sheet1", "A6", "A6", style) +// f := excelize.NewFile() +// f.SetCellValue("Sheet1", "A6", 42920.5) +// exp := "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@" +// style, err := f.NewStyle(&excelize.Style{CustomNumFmt: &exp}) +// err = f.SetCellStyle("Sheet1", "A6", "A6", style) // // Cell Sheet1!A6 in the Excel Application: martes, 04 de Julio de 2017 -// func (f *File) NewStyle(style interface{}) (int, error) { var fs *Style var err error @@ -2476,102 +2475,101 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // For example create a borders of cell H9 on Sheet1: // -// style, err := f.NewStyle(&excelize.Style{ -// Border: []excelize.Border{ -// {Type: "left", Color: "0000FF", Style: 3}, -// {Type: "top", Color: "00FF00", Style: 4}, -// {Type: "bottom", Color: "FFFF00", Style: 5}, -// {Type: "right", Color: "FF0000", Style: 6}, -// {Type: "diagonalDown", Color: "A020F0", Style: 7}, -// {Type: "diagonalUp", Color: "A020F0", Style: 8}, -// }, -// }) -// if err != nil { -// fmt.Println(err) -// } -// err = f.SetCellStyle("Sheet1", "H9", "H9", style) +// style, err := f.NewStyle(&excelize.Style{ +// Border: []excelize.Border{ +// {Type: "left", Color: "0000FF", Style: 3}, +// {Type: "top", Color: "00FF00", Style: 4}, +// {Type: "bottom", Color: "FFFF00", Style: 5}, +// {Type: "right", Color: "FF0000", Style: 6}, +// {Type: "diagonalDown", Color: "A020F0", Style: 7}, +// {Type: "diagonalUp", Color: "A020F0", Style: 8}, +// }, +// }) +// if err != nil { +// fmt.Println(err) +// } +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // // Set gradient fill with vertical variants shading styles for cell H9 on // Sheet1: // -// style, err := f.NewStyle(&excelize.Style{ -// Fill: excelize.Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 1}, -// }) -// if err != nil { -// fmt.Println(err) -// } -// err = f.SetCellStyle("Sheet1", "H9", "H9", style) +// style, err := f.NewStyle(&excelize.Style{ +// Fill: excelize.Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 1}, +// }) +// if err != nil { +// fmt.Println(err) +// } +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // // Set solid style pattern fill for cell H9 on Sheet1: // -// style, err := f.NewStyle(&excelize.Style{ -// Fill: excelize.Fill{Type: "pattern", Color: []string{"#E0EBF5"}, Pattern: 1}, -// }) -// if err != nil { -// fmt.Println(err) -// } -// err = f.SetCellStyle("Sheet1", "H9", "H9", style) +// style, err := f.NewStyle(&excelize.Style{ +// Fill: excelize.Fill{Type: "pattern", Color: []string{"#E0EBF5"}, Pattern: 1}, +// }) +// if err != nil { +// fmt.Println(err) +// } +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // // Set alignment style for cell H9 on Sheet1: // -// style, err := f.NewStyle(&excelize.Style{ -// Alignment: &excelize.Alignment{ -// Horizontal: "center", -// Indent: 1, -// JustifyLastLine: true, -// ReadingOrder: 0, -// RelativeIndent: 1, -// ShrinkToFit: true, -// TextRotation: 45, -// Vertical: "", -// WrapText: true, -// }, -// }) -// if err != nil { -// fmt.Println(err) -// } -// err = f.SetCellStyle("Sheet1", "H9", "H9", style) +// style, err := f.NewStyle(&excelize.Style{ +// Alignment: &excelize.Alignment{ +// Horizontal: "center", +// Indent: 1, +// JustifyLastLine: true, +// ReadingOrder: 0, +// RelativeIndent: 1, +// ShrinkToFit: true, +// TextRotation: 45, +// Vertical: "", +// WrapText: true, +// }, +// }) +// if err != nil { +// fmt.Println(err) +// } +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // // Dates and times in Excel are represented by real numbers, for example "Apr 7 // 2017 12:00 PM" is represented by the number 42920.5. Set date and time format // for cell H9 on Sheet1: // -// f.SetCellValue("Sheet1", "H9", 42920.5) -// style, err := f.NewStyle(&excelize.Style{NumFmt: 22}) -// if err != nil { -// fmt.Println(err) -// } -// err = f.SetCellStyle("Sheet1", "H9", "H9", style) +// f.SetCellValue("Sheet1", "H9", 42920.5) +// style, err := f.NewStyle(&excelize.Style{NumFmt: 22}) +// if err != nil { +// fmt.Println(err) +// } +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // // Set font style for cell H9 on Sheet1: // -// style, err := f.NewStyle(&excelize.Style{ -// Font: &excelize.Font{ -// Bold: true, -// Italic: true, -// Family: "Times New Roman", -// Size: 36, -// Color: "#777777", -// }, -// }) -// if err != nil { -// fmt.Println(err) -// } -// err = f.SetCellStyle("Sheet1", "H9", "H9", style) +// style, err := f.NewStyle(&excelize.Style{ +// Font: &excelize.Font{ +// Bold: true, +// Italic: true, +// Family: "Times New Roman", +// Size: 36, +// Color: "#777777", +// }, +// }) +// if err != nil { +// fmt.Println(err) +// } +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // // Hide and lock for cell H9 on Sheet1: // -// style, err := f.NewStyle(&excelize.Style{ -// Protection: &excelize.Protection{ -// Hidden: true, -// Locked: true, -// }, -// }) -// if err != nil { -// fmt.Println(err) -// } -// err = f.SetCellStyle("Sheet1", "H9", "H9", style) -// +// style, err := f.NewStyle(&excelize.Style{ +// Protection: &excelize.Protection{ +// Hidden: true, +// Locked: true, +// }, +// }) +// if err != nil { +// fmt.Println(err) +// } +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { hCol, hRow, err := CellNameToCoordinates(hCell) if err != nil { @@ -2622,64 +2620,64 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // The type option is a required parameter and it has no default value. // Allowable type values and their associated parameters are: // -// Type | Parameters -// ---------------+------------------------------------ -// cell | criteria -// | value -// | minimum -// | maximum -// date | criteria -// | value -// | minimum -// | maximum -// time_period | criteria -// text | criteria -// | value -// average | criteria -// duplicate | (none) -// unique | (none) -// top | criteria -// | value -// bottom | criteria -// | value -// blanks | (none) -// no_blanks | (none) -// errors | (none) -// no_errors | (none) -// 2_color_scale | min_type -// | max_type -// | min_value -// | max_value -// | min_color -// | max_color -// 3_color_scale | min_type -// | mid_type -// | max_type -// | min_value -// | mid_value -// | max_value -// | min_color -// | mid_color -// | max_color -// data_bar | min_type -// | max_type -// | min_value -// | max_value -// | bar_color -// formula | criteria +// Type | Parameters +// ---------------+------------------------------------ +// cell | criteria +// | value +// | minimum +// | maximum +// date | criteria +// | value +// | minimum +// | maximum +// time_period | criteria +// text | criteria +// | value +// average | criteria +// duplicate | (none) +// unique | (none) +// top | criteria +// | value +// bottom | criteria +// | value +// blanks | (none) +// no_blanks | (none) +// errors | (none) +// no_errors | (none) +// 2_color_scale | min_type +// | max_type +// | min_value +// | max_value +// | min_color +// | max_color +// 3_color_scale | min_type +// | mid_type +// | max_type +// | min_value +// | mid_value +// | max_value +// | min_color +// | mid_color +// | max_color +// data_bar | min_type +// | max_type +// | min_value +// | max_value +// | bar_color +// formula | criteria // // The criteria parameter is used to set the criteria by which the cell data // will be evaluated. It has no default value. The most common criteria as // applied to {"type":"cell"} are: // -// between | -// not between | -// equal to | == -// not equal to | != -// greater than | > -// less than | < -// greater than or equal to | >= -// less than or equal to | <= +// between | +// not between | +// equal to | == +// not equal to | != +// greater than | > +// less than | < +// greater than or equal to | >= +// less than or equal to | <= // // You can either use Excel's textual description strings, in the first column // above, or the more common symbolic alternatives. @@ -2690,22 +2688,22 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // value: The value is generally used along with the criteria parameter to set // the rule by which the cell data will be evaluated: // -// f.SetConditionalFormat("Sheet1", "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)) +// f.SetConditionalFormat("Sheet1", "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)) // // The value property can also be an cell reference: // -// f.SetConditionalFormat("Sheet1", "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"$C$1"}]`, format)) +// f.SetConditionalFormat("Sheet1", "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"$C$1"}]`, format)) // // type: format - The format parameter is used to specify the format that will // be applied to the cell when the conditional formatting criterion is met. The // format is created using the NewConditionalStyle() method in the same way as // cell formats: // -// format, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) -// if err != nil { -// fmt.Println(err) -// } -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)) +// format, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) +// if err != nil { +// fmt.Println(err) +// } +// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)) // // Note: In Excel, a conditional format is superimposed over the existing cell // format and not all cell format properties can be modified. Properties that @@ -2716,20 +2714,20 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // Excel specifies some default formats to be used with conditional formatting. // These can be replicated using the following excelize formats: // -// // Rose format for bad conditional. -// format1, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) +// // Rose format for bad conditional. +// format1, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) // -// // Light yellow format for neutral conditional. -// format2, err = f.NewConditionalStyle(`{"font":{"color":"#9B5713"},"fill":{"type":"pattern","color":["#FEEAA0"],"pattern":1}}`) +// // Light yellow format for neutral conditional. +// format2, err = f.NewConditionalStyle(`{"font":{"color":"#9B5713"},"fill":{"type":"pattern","color":["#FEEAA0"],"pattern":1}}`) // -// // Light green format for good conditional. -// format3, err = f.NewConditionalStyle(`{"font":{"color":"#09600B"},"fill":{"type":"pattern","color":["#C7EECF"],"pattern":1}}`) +// // Light green format for good conditional. +// format3, err = f.NewConditionalStyle(`{"font":{"color":"#09600B"},"fill":{"type":"pattern","color":["#C7EECF"],"pattern":1}}`) // // type: minimum - The minimum parameter is used to set the lower limiting value // when the criteria is either "between" or "not between". // -// // Hightlight cells rules: between... -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format)) +// // Hightlight cells rules: between... +// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format)) // // type: maximum - The maximum parameter is used to set the upper limiting value // when the criteria is either "between" or "not between". See the previous @@ -2738,36 +2736,36 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // type: average - The average type is used to specify Excel's "Average" style // conditional format: // -// // Top/Bottom rules: Above Average... -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format1)) +// // Top/Bottom rules: Above Average... +// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format1)) // -// // Top/Bottom rules: Below Average... -// f.SetConditionalFormat("Sheet1", "B1:B10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format2)) +// // Top/Bottom rules: Below Average... +// f.SetConditionalFormat("Sheet1", "B1:B10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format2)) // // type: duplicate - The duplicate type is used to highlight duplicate cells in a range: // -// // Hightlight cells rules: Duplicate Values... -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format)) +// // Hightlight cells rules: Duplicate Values... +// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format)) // // type: unique - The unique type is used to highlight unique cells in a range: // -// // Hightlight cells rules: Not Equal To... -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format)) +// // Hightlight cells rules: Not Equal To... +// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format)) // // type: top - The top type is used to specify the top n values by number or percentage in a range: // -// // Top/Bottom rules: Top 10. -// f.SetConditionalFormat("Sheet1", "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6"}]`, format)) +// // Top/Bottom rules: Top 10. +// f.SetConditionalFormat("Sheet1", "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6"}]`, format)) // // The criteria can be used to indicate that a percentage condition is required: // -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format)) +// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format)) // // type: 2_color_scale - The 2_color_scale type is used to specify Excel's "2 // Color Scale" style conditional format: // -// // Color scales: 2 color. -// f.SetConditionalFormat("Sheet1", "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`) +// // Color scales: 2 color. +// f.SetConditionalFormat("Sheet1", "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`) // // This conditional type can be modified with min_type, max_type, min_value, // max_value, min_color and max_color, see below. @@ -2775,8 +2773,8 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // type: 3_color_scale - The 3_color_scale type is used to specify Excel's "3 // Color Scale" style conditional format: // -// // Color scales: 3 color. -// f.SetConditionalFormat("Sheet1", "A1:A10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`) +// // Color scales: 3 color. +// f.SetConditionalFormat("Sheet1", "A1:A10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`) // // This conditional type can be modified with min_type, mid_type, max_type, // min_value, mid_value, max_value, min_color, mid_color and max_color, see @@ -2787,17 +2785,17 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // // min_type - The min_type and max_type properties are available when the conditional formatting type is 2_color_scale, 3_color_scale or data_bar. The mid_type is available for 3_color_scale. The properties are used as follows: // -// // Data Bars: Gradient Fill. -// f.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) +// // Data Bars: Gradient Fill. +// f.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) // // The available min/mid/max types are: // -// min (for min_type only) -// num -// percent -// percentile -// formula -// max (for max_type only) +// min (for min_type only) +// num +// percent +// percentile +// formula +// max (for max_type only) // // mid_type - Used for 3_color_scale. Same as min_type, see above. // @@ -2816,15 +2814,14 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // The mid_color is available for 3_color_scale. The properties are used as // follows: // -// // Color scales: 3 color. -// f.SetConditionalFormat("Sheet1", "B1:B10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`) +// // Color scales: 3 color. +// f.SetConditionalFormat("Sheet1", "B1:B10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`) // // mid_color - Used for 3_color_scale. Same as min_color, see above. // // max_color - Same as min_color, see above. // // bar_color - Used for data_bar. Same as min_color, see above. -// func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { var format []*formatConditional err := json.Unmarshal([]byte(formatSet), &format) diff --git a/table.go b/table.go index cf9c22a5d9..dc5f441970 100644 --- a/table.go +++ b/table.go @@ -32,18 +32,18 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) { // name, coordinate area and format set. For example, create a table of A1:D5 // on Sheet1: // -// err := f.AddTable("Sheet1", "A1", "D5", "") +// err := f.AddTable("Sheet1", "A1", "D5", "") // // Create a table of F2:H6 on Sheet2 with format set: // -// err := f.AddTable("Sheet2", "F2", "H6", `{ -// "table_name": "table", -// "table_style": "TableStyleMedium2", -// "show_first_column": true, -// "show_last_column": true, -// "show_row_stripes": false, -// "show_column_stripes": true -// }`) +// err := f.AddTable("Sheet2", "F2", "H6", `{ +// "table_name": "table", +// "table_style": "TableStyleMedium2", +// "show_first_column": true, +// "show_last_column": true, +// "show_row_stripes": false, +// "show_column_stripes": true +// }`) // // Note that the table must be at least two lines including the header. The // header cells must contain strings and must be unique, and must set the @@ -54,10 +54,9 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) { // // table_style: The built-in table style names // -// TableStyleLight1 - TableStyleLight21 -// TableStyleMedium1 - TableStyleMedium28 -// TableStyleDark1 - TableStyleDark11 -// +// TableStyleLight1 - TableStyleLight21 +// TableStyleMedium1 - TableStyleMedium28 +// TableStyleDark1 - TableStyleDark11 func (f *File) AddTable(sheet, hCell, vCell, format string) error { formatSet, err := parseFormatTableSet(format) if err != nil { @@ -216,11 +215,11 @@ func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { // way of filtering a 2D range of data based on some simple criteria. For // example applying an autofilter to a cell range A1:D4 in the Sheet1: // -// err := f.AutoFilter("Sheet1", "A1", "D4", "") +// err := f.AutoFilter("Sheet1", "A1", "D4", "") // // Filter data in an autofilter: // -// err := f.AutoFilter("Sheet1", "A1", "D4", `{"column":"B","expression":"x != blanks"}`) +// err := f.AutoFilter("Sheet1", "A1", "D4", `{"column":"B","expression":"x != blanks"}`) // // column defines the filter columns in a autofilter range based on simple // criteria @@ -235,38 +234,38 @@ func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { // expression defines the conditions, the following operators are available // for setting the filter criteria: // -// == -// != -// > -// < -// >= -// <= -// and -// or +// == +// != +// > +// < +// >= +// <= +// and +// or // // An expression can comprise a single statement or two statements separated // by the 'and' and 'or' operators. For example: // -// x < 2000 -// x > 2000 -// x == 2000 -// x > 2000 and x < 5000 -// x == 2000 or x == 5000 +// x < 2000 +// x > 2000 +// x == 2000 +// x > 2000 and x < 5000 +// x == 2000 or x == 5000 // // Filtering of blank or non-blank data can be achieved by using a value of // Blanks or NonBlanks in the expression: // -// x == Blanks -// x == NonBlanks +// x == Blanks +// x == NonBlanks // // Excel also allows some simple string matching operations: // -// x == b* // begins with b -// x != b* // doesn't begin with b -// x == *b // ends with b -// x != *b // doesn't end with b -// x == *b* // contains b -// x != *b* // doesn't contains b +// x == b* // begins with b +// x != b* // doesn't begin with b +// x == *b // ends with b +// x != *b // doesn't end with b +// x == *b* // contains b +// x != *b* // doesn't contains b // // You can also use '*' to match any character or number and '?' to match any // single character or number. No other regular expression quantifier is @@ -277,10 +276,9 @@ func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { // simple string. The actual placeholder name is ignored internally so the // following are all equivalent: // -// x < 2000 -// col < 2000 -// Price < 2000 -// +// x < 2000 +// col < 2000 +// Price < 2000 func (f *File) AutoFilter(sheet, hCell, vCell, format string) error { hCol, hRow, err := CellNameToCoordinates(hCell) if err != nil { @@ -436,9 +434,8 @@ func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val strin // // Examples: // -// ('x', '==', 2000) -> exp1 -// ('x', '>', 2000, 'and', 'x', '<', 5000) -> exp1 and exp2 -// +// ('x', '==', 2000) -> exp1 +// ('x', '>', 2000, 'and', 'x', '<', 5000) -> exp1 and exp2 func (f *File) parseFilterExpression(expression string, tokens []string) ([]int, []string, error) { var expressions []int var t []string diff --git a/workbook.go b/workbook.go index 417524b1da..dbe212acca 100644 --- a/workbook.go +++ b/workbook.go @@ -120,9 +120,10 @@ func (f *File) workBookWriter() { // SetWorkbookPrOptions provides a function to sets workbook properties. // // Available options: -// Date1904(bool) -// FilterPrivacy(bool) -// CodeName(string) +// +// Date1904(bool) +// FilterPrivacy(bool) +// CodeName(string) func (f *File) SetWorkbookPrOptions(opts ...WorkbookPrOption) error { wb := f.workbookReader() pr := wb.WorkbookPr @@ -154,9 +155,10 @@ func (o CodeName) setWorkbookPrOption(pr *xlsxWorkbookPr) { // GetWorkbookPrOptions provides a function to gets workbook properties. // // Available options: -// Date1904(bool) -// FilterPrivacy(bool) -// CodeName(string) +// +// Date1904(bool) +// FilterPrivacy(bool) +// CodeName(string) func (f *File) GetWorkbookPrOptions(opts ...WorkbookPrOptionPtr) error { wb := f.workbookReader() pr := wb.WorkbookPr diff --git a/xmlCalcChain.go b/xmlCalcChain.go index f578033953..9e25d50795 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -21,60 +21,59 @@ type xlsxCalcChain struct { // xlsxCalcChainC directly maps the c element. // -// Attributes | Attributes -// --------------------------+---------------------------------------------------------- -// a (Array) | A Boolean flag indicating whether the cell's formula -// | is an array formula. True if this cell's formula is -// | an array formula, false otherwise. If there is a -// | conflict between this attribute and the t attribute -// | of the f element (§18.3.1.40), the t attribute takes -// | precedence. The possible values for this attribute -// | are defined by the W3C XML Schema boolean datatype. -// | -// i (Sheet Id) | A sheet Id of a sheet the cell belongs to. If this is -// | omitted, it is assumed to be the same as the i value -// | of the previous cell.The possible values for this -// | attribute are defined by the W3C XML Schema int datatype. -// | -// l (New Dependency Level) | A Boolean flag indicating that the cell's formula -// | starts a new dependency level. True if the formula -// | starts a new dependency level, false otherwise. -// | Starting a new dependency level means that all -// | concurrent calculations, and child calculations, shall -// | be completed - and the cells have new values - before -// | the calc chain can continue. In other words, this -// | dependency level might depend on levels that came before -// | it, and any later dependency levels might depend on -// | this level; but not later dependency levels can have -// | any calculations started until this dependency level -// | completes.The possible values for this attribute are -// | defined by the W3C XML Schema boolean datatype. -// | -// r (Cell Reference) | An A-1 style reference to a cell.The possible values -// | for this attribute are defined by the ST_CellRef -// | simple type (§18.18.7). -// | -// s (Child Chain) | A Boolean flag indicating whether the cell's formula -// | is on a child chain. True if this cell is part of a -// | child chain, false otherwise. If this is omitted, it -// | is assumed to be the same as the s value of the -// | previous cell .A child chain is a list of calculations -// | that occur which depend on the parent to the chain. -// | There shall not be cross dependencies between child -// | chains. Child chains are not the same as dependency -// | levels - a child chain and its parent are all on the -// | same dependency level. Child chains are series of -// | calculations that can be independently farmed out to -// | other threads or processors.The possible values for -// | this attribute is defined by the W3C XML Schema -// | boolean datatype. -// | -// t (New Thread) | A Boolean flag indicating whether the cell's formula -// | starts a new thread. True if the cell's formula starts -// | a new thread, false otherwise.The possible values for -// | this attribute is defined by the W3C XML Schema -// | boolean datatype. -// +// Attributes | Attributes +// --------------------------+---------------------------------------------------------- +// a (Array) | A Boolean flag indicating whether the cell's formula +// | is an array formula. True if this cell's formula is +// | an array formula, false otherwise. If there is a +// | conflict between this attribute and the t attribute +// | of the f element (§18.3.1.40), the t attribute takes +// | precedence. The possible values for this attribute +// | are defined by the W3C XML Schema boolean datatype. +// | +// i (Sheet Id) | A sheet Id of a sheet the cell belongs to. If this is +// | omitted, it is assumed to be the same as the i value +// | of the previous cell.The possible values for this +// | attribute are defined by the W3C XML Schema int datatype. +// | +// l (New Dependency Level) | A Boolean flag indicating that the cell's formula +// | starts a new dependency level. True if the formula +// | starts a new dependency level, false otherwise. +// | Starting a new dependency level means that all +// | concurrent calculations, and child calculations, shall +// | be completed - and the cells have new values - before +// | the calc chain can continue. In other words, this +// | dependency level might depend on levels that came before +// | it, and any later dependency levels might depend on +// | this level; but not later dependency levels can have +// | any calculations started until this dependency level +// | completes.The possible values for this attribute are +// | defined by the W3C XML Schema boolean datatype. +// | +// r (Cell Reference) | An A-1 style reference to a cell.The possible values +// | for this attribute are defined by the ST_CellRef +// | simple type (§18.18.7). +// | +// s (Child Chain) | A Boolean flag indicating whether the cell's formula +// | is on a child chain. True if this cell is part of a +// | child chain, false otherwise. If this is omitted, it +// | is assumed to be the same as the s value of the +// | previous cell .A child chain is a list of calculations +// | that occur which depend on the parent to the chain. +// | There shall not be cross dependencies between child +// | chains. Child chains are not the same as dependency +// | levels - a child chain and its parent are all on the +// | same dependency level. Child chains are series of +// | calculations that can be independently farmed out to +// | other threads or processors.The possible values for +// | this attribute is defined by the W3C XML Schema +// | boolean datatype. +// | +// t (New Thread) | A Boolean flag indicating whether the cell's formula +// | starts a new thread. True if the cell's formula starts +// | a new thread, false otherwise.The possible values for +// | this attribute is defined by the W3C XML Schema +// | boolean datatype. type xlsxCalcChainC struct { R string `xml:"r,attr"` I int `xml:"i,attr"` diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 81e9ff926f..3b9caac533 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -449,16 +449,15 @@ type DataValidation struct { // // This simple type is restricted to the values listed in the following table: // -// Enumeration Value | Description -// ---------------------------+--------------------------------- -// b (Boolean) | Cell containing a boolean. -// d (Date) | Cell contains a date in the ISO 8601 format. -// e (Error) | Cell containing an error. -// inlineStr (Inline String) | Cell containing an (inline) rich string, i.e., one not in the shared string table. If this cell type is used, then the cell value is in the is element rather than the v element in the cell (c element). -// n (Number) | Cell containing a number. -// s (Shared String) | Cell containing a shared string. -// str (String) | Cell containing a formula string. -// +// Enumeration Value | Description +// ---------------------------+--------------------------------- +// b (Boolean) | Cell containing a boolean. +// d (Date) | Cell contains a date in the ISO 8601 format. +// e (Error) | Cell containing an error. +// inlineStr (Inline String) | Cell containing an (inline) rich string, i.e., one not in the shared string table. If this cell type is used, then the cell value is in the is element rather than the v element in the cell (c element). +// n (Number) | Cell containing a number. +// s (Shared String) | Cell containing a shared string. +// str (String) | Cell containing a formula string. type xlsxC struct { XMLName xml.Name `xml:"c"` XMLSpace xml.Attr `xml:"space,attr,omitempty"` @@ -644,13 +643,12 @@ type xlsxHyperlink struct { // size of the sample. To reference the table, just add the tableParts element, // of course after having created and stored the table part. For example: // -// -// ... -// -// -// -// -// +// +// ... +// +// +// +// type xlsxTableParts struct { XMLName xml.Name `xml:"tableParts"` Count int `xml:"count,attr,omitempty"` @@ -667,8 +665,7 @@ type xlsxTablePart struct { // http://schemas.openxmlformats.org/spreadsheetml/2006/main - Background sheet // image. For example: // -// -// +// type xlsxPicture struct { XMLName xml.Name `xml:"picture"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` From d1e76fc432ac5c9bde99591ec5e88e46b62d9c3d Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 17 Aug 2022 10:59:52 +0800 Subject: [PATCH 088/213] This closes #1319, fix calculate error for formula with negative symbol - Update unit test and comment for the functions --- calc.go | 8 ++++---- calc_test.go | 1 + comment.go | 2 +- rows.go | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/calc.go b/calc.go index 0cdb91ed6d..25595d6850 100644 --- a/calc.go +++ b/calc.go @@ -1234,7 +1234,7 @@ func calculate(opdStack *Stack, opt efp.Token) error { return ErrInvalidFormula } opd := opdStack.Pop().(formulaArg) - opdStack.Push(newNumberFormulaArg(0 - opd.Number)) + opdStack.Push(newNumberFormulaArg(0 - opd.ToNumber().Number)) } if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { if opdStack.Len() < 2 { @@ -1647,10 +1647,10 @@ 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 + percentile := 1.0 if strings.HasSuffix(cond, "%") { cond = strings.TrimSuffix(cond, "%") - percential /= 100 + percentile /= 100 } if value, err = strconv.ParseFloat(val, 64); err != nil { return @@ -1658,7 +1658,7 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er if expected, err = strconv.ParseFloat(cond, 64); err != nil { return } - expected *= percential + expected *= percentile return } switch criteria.Type { diff --git a/calc_test.go b/calc_test.go index 47cd8067b2..5822135f18 100644 --- a/calc_test.go +++ b/calc_test.go @@ -491,6 +491,7 @@ func TestCalcCellValue(t *testing.T) { // COS "=COS(0.785398163)": "0.707106781467586", "=COS(0)": "1", + "=-COS(0)": "-1", "=COS(COS(0))": "0.54030230586814", // COSH "=COSH(0)": "1", diff --git a/comment.go b/comment.go index 0794986156..82d1f88cf0 100644 --- a/comment.go +++ b/comment.go @@ -177,7 +177,7 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, }, }, } - // load exist comment shapes from xl/drawings/vmlDrawing%d.vml (only once) + // load exist comment shapes from xl/drawings/vmlDrawing%d.vml d := f.decodeVMLDrawingReader(drawingVML) if d != nil { for _, v := range d.Shape { diff --git a/rows.go b/rows.go index 9eef6286b0..58085302a3 100644 --- a/rows.go +++ b/rows.go @@ -179,6 +179,7 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) { return rowIterator.columns, rowIterator.err } +// extractRowOpts extract row element attributes. func extractRowOpts(attrs []xml.Attr) RowOpts { rowOpts := RowOpts{Height: defaultRowHeight} if styleID, err := attrValToInt("s", attrs); err == nil && styleID > 0 && styleID < MaxCellStyles { From 76f336809f5419343702de5b3284d46feb9ed266 Mon Sep 17 00:00:00 2001 From: NaturalGao <43291304+NaturalGao@users.noreply.github.com> Date: Fri, 19 Aug 2022 23:24:13 +0800 Subject: [PATCH 089/213] This closes #849, add new function `DeleteComment` for delete comment (#1317) - Update unit tests for the delete comment - Add 3 errors function for error messages --- comment.go | 33 +++++++++++++++++++++++++++++++++ comment_test.go | 26 ++++++++++++++++++++++++++ docProps.go | 9 ++++----- drawing.go | 3 +-- errors.go | 17 +++++++++++++++++ excelize.go | 8 ++++---- picture.go | 5 ++--- stream.go | 2 +- 8 files changed, 88 insertions(+), 15 deletions(-) diff --git a/comment.go b/comment.go index 82d1f88cf0..ac22ec7476 100644 --- a/comment.go +++ b/comment.go @@ -140,6 +140,39 @@ func (f *File) AddComment(sheet, cell, format string) error { return err } +// DeleteComment provides the method to delete comment in a sheet by given +// worksheet. For example, delete the comment in Sheet1!$A$30: +// +// err := f.DeleteComment("Sheet1", "A30") +func (f *File) DeleteComment(sheet, cell string) (err error) { + sheetXMLPath, ok := f.getSheetXMLPath(sheet) + if !ok { + err = newNoExistSheetError(sheet) + return + } + commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath)) + if !strings.HasPrefix(commentsXML, "/") { + commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..") + } + commentsXML = strings.TrimPrefix(commentsXML, "/") + if comments := f.commentsReader(commentsXML); comments != nil { + for i, cmt := range comments.CommentList.Comment { + if cmt.Ref == cell { + if len(comments.CommentList.Comment) > 1 { + comments.CommentList.Comment = append( + comments.CommentList.Comment[:i], + comments.CommentList.Comment[i+1:]..., + ) + continue + } + comments.CommentList.Comment = nil + } + } + f.Comments[commentsXML] = comments + } + return +} + // addDrawingVML provides a function to create comment as // xl/drawings/vmlDrawing%d.vml by given commit ID and cell. func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) error { diff --git a/comment_test.go b/comment_test.go index 01f1e42e3e..c2d9fe2eda 100644 --- a/comment_test.go +++ b/comment_test.go @@ -46,6 +46,32 @@ func TestAddComments(t *testing.T) { assert.EqualValues(t, len(NewFile().GetComments()), 0) } +func TestDeleteComment(t *testing.T) { + f, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() + } + + assert.NoError(t, f.AddComment("Sheet2", "A40", `{"author":"Excelize: ","text":"This is a comment1."}`)) + assert.NoError(t, f.AddComment("Sheet2", "A41", `{"author":"Excelize: ","text":"This is a comment2."}`)) + assert.NoError(t, f.AddComment("Sheet2", "C41", `{"author":"Excelize: ","text":"This is a comment3."}`)) + + assert.NoError(t, f.DeleteComment("Sheet2", "A40")) + + assert.EqualValues(t, 2, len(f.GetComments()["Sheet2"])) + assert.EqualValues(t, len(NewFile().GetComments()), 0) + + // Test delete all comments in a worksheet + assert.NoError(t, f.DeleteComment("Sheet2", "A41")) + assert.NoError(t, f.DeleteComment("Sheet2", "C41")) + assert.EqualValues(t, 0, len(f.GetComments()["Sheet2"])) + // Test delete comment on not exists worksheet + assert.EqualError(t, f.DeleteComment("SheetN", "A1"), "sheet SheetN is not exist") + // Test delete comment with worksheet part + f.Pkg.Delete("xl/worksheets/sheet1.xml") + assert.NoError(t, f.DeleteComment("Sheet1", "A22")) +} + func TestDecodeVMLDrawingReader(t *testing.T) { f := NewFile() path := "xl/drawings/vmlDrawing1.xml" diff --git a/docProps.go b/docProps.go index df15b57dc8..00ff808bbd 100644 --- a/docProps.go +++ b/docProps.go @@ -14,7 +14,6 @@ package excelize import ( "bytes" "encoding/xml" - "fmt" "io" "reflect" ) @@ -76,7 +75,7 @@ func (f *File) SetAppProps(appProperties *AppProperties) (err error) { app = new(xlsxProperties) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsApp)))). Decode(app); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) + err = newDecodeXMLError(err) return } fields = []string{"Application", "ScaleCrop", "DocSecurity", "Company", "LinksUpToDate", "HyperlinksChanged", "AppVersion"} @@ -103,7 +102,7 @@ func (f *File) GetAppProps() (ret *AppProperties, err error) { app := new(xlsxProperties) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsApp)))). Decode(app); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) + err = newDecodeXMLError(err) return } ret, err = &AppProperties{ @@ -181,7 +180,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { core = new(decodeCoreProperties) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))). Decode(core); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) + err = newDecodeXMLError(err) return } newProps, err = &xlsxCoreProperties{ @@ -236,7 +235,7 @@ func (f *File) GetDocProps() (ret *DocProperties, err error) { if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))). Decode(core); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) + err = newDecodeXMLError(err) return } ret, err = &DocProperties{ diff --git a/drawing.go b/drawing.go index 10f4cd0f2c..5015d265bf 100644 --- a/drawing.go +++ b/drawing.go @@ -14,7 +14,6 @@ package excelize import ( "bytes" "encoding/xml" - "fmt" "io" "log" "reflect" @@ -1322,7 +1321,7 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (err deTwoCellAnchor = new(decodeTwoCellAnchor) if err = f.xmlNewDecoder(strings.NewReader("" + wsDr.TwoCellAnchor[idx].GraphicFrame + "")). Decode(deTwoCellAnchor); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) + err = newDecodeXMLError(err) return } if err = nil; deTwoCellAnchor.From != nil && decodeTwoCellAnchorFuncs[drawingType](deTwoCellAnchor) { diff --git a/errors.go b/errors.go index f5ea06e658..fbcef043a6 100644 --- a/errors.go +++ b/errors.go @@ -70,6 +70,23 @@ func newCellNameToCoordinatesError(cell string, err error) error { return fmt.Errorf("cannot convert cell %q to coordinates: %v", cell, err) } +// newNoExistSheetError defined the error message on receiving the not exist +// sheet name. +func newNoExistSheetError(name string) error { + return fmt.Errorf("sheet %s is not exist", name) +} + +// newNotWorksheetError defined the error message on receiving a sheet which +// not a worksheet. +func newNotWorksheetError(name string) error { + return fmt.Errorf("sheet %s is not a worksheet", name) +} + +// newDecodeXMLError defined the error message on decode XML error. +func newDecodeXMLError(err error) error { + return fmt.Errorf("xml decode error: %s", err) +} + var ( // ErrStreamSetColWidth defined the error message on set column width in // stream writing mode. diff --git a/excelize.go b/excelize.go index ef438dd8dd..f3b4381da2 100644 --- a/excelize.go +++ b/excelize.go @@ -231,7 +231,7 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { ok bool ) if name, ok = f.getSheetXMLPath(sheet); !ok { - err = fmt.Errorf("sheet %s is not exist", sheet) + err = newNoExistSheetError(sheet) return } if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil { @@ -240,7 +240,7 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { } for _, sheetType := range []string{"xl/chartsheets", "xl/dialogsheet", "xl/macrosheet"} { if strings.HasPrefix(name, sheetType) { - err = fmt.Errorf("sheet %s is not a worksheet", sheet) + err = newNotWorksheetError(sheet) return } } @@ -251,7 +251,7 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))). Decode(ws); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) + err = newDecodeXMLError(err) return } err = nil @@ -424,7 +424,7 @@ func (f *File) UpdateLinkedValue() error { for _, name := range f.GetSheetList() { ws, err := f.workSheetReader(name) if err != nil { - if err.Error() == fmt.Sprintf("sheet %s is not a worksheet", trimSheetName(name)) { + if err.Error() == newNotWorksheetError(name).Error() { continue } return err diff --git a/picture.go b/picture.go index c78df93cf3..84c7731681 100644 --- a/picture.go +++ b/picture.go @@ -15,7 +15,6 @@ import ( "bytes" "encoding/json" "encoding/xml" - "fmt" "image" "io" "io/ioutil" @@ -554,7 +553,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) deWsDr = new(decodeWsDr) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). Decode(deWsDr); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) + err = newDecodeXMLError(err) return } err = nil @@ -562,7 +561,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) deTwoCellAnchor = new(decodeTwoCellAnchor) if err = f.xmlNewDecoder(strings.NewReader("" + anchor.Content + "")). Decode(deTwoCellAnchor); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) + err = newDecodeXMLError(err) return } if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic != nil { diff --git a/stream.go b/stream.go index 91ae78a45f..3c05c327af 100644 --- a/stream.go +++ b/stream.go @@ -92,7 +92,7 @@ type StreamWriter struct { func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { sheetID := f.getSheetID(sheet) if sheetID == -1 { - return nil, fmt.Errorf("sheet %s is not exist", sheet) + return nil, newNoExistSheetError(sheet) } sw := &StreamWriter{ File: f, From cfa2d603ddb0fac50ca1af3fe1d28fe17a65a6f3 Mon Sep 17 00:00:00 2001 From: Sangua633 <76948439+Sangua633@users.noreply.github.com> Date: Sat, 20 Aug 2022 15:51:03 +0800 Subject: [PATCH 090/213] Support encrypt workbook with password #199 (#1324) --- crypt.go | 886 ++++++++++++++++++-------------------------------- crypt_test.go | 5 - 2 files changed, 316 insertions(+), 575 deletions(-) diff --git a/crypt.go b/crypt.go index a5670ac2fd..58a1c99bf6 100644 --- a/crypt.go +++ b/crypt.go @@ -25,7 +25,9 @@ import ( "encoding/xml" "hash" "math" + "path/filepath" "reflect" + "sort" "strings" "github.com/richardlehane/mscfb" @@ -37,6 +39,10 @@ import ( var ( blockKey = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption oleIdentifier = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1} + headerCLSID = make([]byte, 16) + difSect = -4 + endOfChain = -2 + fatSect = -3 iterCount = 50000 packageEncryptionChunkSize = 4096 packageOffset = 8 // First 8 bytes are the size of the stream @@ -150,7 +156,7 @@ func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { } // Encrypt API encrypt data with the password. -func Encrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { +func Encrypt(raw []byte, opt *Options) ([]byte, error) { encryptor := encryption{ EncryptedVerifierHashInput: make([]byte, 16), EncryptedVerifierHashValue: make([]byte, 32), @@ -169,9 +175,13 @@ func Encrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { binary.LittleEndian.PutUint64(encryptedPackage, uint64(len(raw))) encryptedPackage = append(encryptedPackage, encryptor.encrypt(raw)...) // Create a new CFB - compoundFile := cfb{} - packageBuf = compoundFile.Writer(encryptionInfoBuffer, encryptedPackage) - return packageBuf, nil + compoundFile := &cfb{ + paths: []string{"Root Entry/"}, + sectors: []sector{{name: "Root Entry", typeID: 5}}, + } + compoundFile.put("EncryptionInfo", encryptionInfoBuffer) + compoundFile.put("EncryptedPackage", encryptedPackage) + return compoundFile.write(), nil } // extractPart extract data from storage by specified part name. @@ -618,6 +628,15 @@ func genISOPasswdHash(passwd, hashAlgorithm, salt string, spinCount int) (hashVa type cfb struct { stream []byte position int + paths []string + sectors []sector +} + +// sector structure used for FAT, directory, miniFAT, and miniStream sectors. +type sector struct { + clsID, content []byte + name string + C, L, R, color, size, start, state, typeID int } // writeBytes write bytes in the stream by a given value with an offset. @@ -666,415 +685,156 @@ func (c *cfb) writeStrings(value string) { c.writeBytes(buffer) } -// writeVersionStream provides a function to write compound file version -// stream. -func (c *cfb) writeVersionStream() []byte { - var storage cfb - storage.writeUint32(0x3c) - storage.writeStrings("Microsoft.Container.DataSpaces") - storage.writeUint32(0x01) - storage.writeUint32(0x01) - storage.writeUint32(0x01) - return storage.stream -} - -// writeDataSpaceMapStream provides a function to write compound file -// DataSpaceMap stream. -func (c *cfb) writeDataSpaceMapStream() []byte { - var storage cfb - storage.writeUint32(0x08) - storage.writeUint32(0x01) - storage.writeUint32(0x68) - storage.writeUint32(0x01) - storage.writeUint32(0x00) - storage.writeUint32(0x20) - storage.writeStrings("EncryptedPackage") - storage.writeUint32(0x32) - storage.writeStrings("StrongEncryptionDataSpace") - storage.writeUint16(0x00) - return storage.stream -} - -// writeStrongEncryptionDataSpaceStream provides a function to write compound -// file StrongEncryptionDataSpace stream. -func (c *cfb) writeStrongEncryptionDataSpaceStream() []byte { - var storage cfb - storage.writeUint32(0x08) - storage.writeUint32(0x01) - storage.writeUint32(0x32) - storage.writeStrings("StrongEncryptionTransform") - storage.writeUint16(0x00) - return storage.stream -} - -// writePrimaryStream provides a function to write compound file Primary -// stream. -func (c *cfb) writePrimaryStream() []byte { - var storage cfb - storage.writeUint32(0x6C) - storage.writeUint32(0x01) - storage.writeUint32(0x4C) - storage.writeStrings("{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}") - storage.writeUint32(0x4E) - storage.writeUint16(0x00) - storage.writeUint32(0x01) - storage.writeUint32(0x01) - storage.writeUint32(0x01) - storage.writeStrings("AES128") - storage.writeUint32(0x00) - storage.writeUint32(0x04) - return storage.stream -} - -// writeFileStream provides a function to write encrypted package in compound -// file by a given buffer and the short sector allocation table. -func (c *cfb) writeFileStream(encryptionInfoBuffer []byte, SSAT []int) ([]byte, []int) { - var ( - storage cfb - miniProperties int - stream = make([]byte, 0x100) - ) - if encryptionInfoBuffer != nil { - copy(stream, encryptionInfoBuffer) - } - storage.writeBytes(stream) - streamBlocks := len(stream) / 64 - if len(stream)%64 > 0 { - streamBlocks++ - } - for i := 1; i < streamBlocks; i++ { - SSAT = append(SSAT, i) - } - SSAT = append(SSAT, -2) - miniProperties += streamBlocks - versionStream := make([]byte, 0x80) - version := c.writeVersionStream() - copy(versionStream, version) - storage.writeBytes(versionStream) - versionBlocks := len(versionStream) / 64 - if len(versionStream)%64 > 0 { - versionBlocks++ - } - for i := 1; i < versionBlocks; i++ { - SSAT = append(SSAT, i+miniProperties) - } - SSAT = append(SSAT, -2) - miniProperties += versionBlocks - dataSpaceMap := make([]byte, 0x80) - dataStream := c.writeDataSpaceMapStream() - copy(dataSpaceMap, dataStream) - storage.writeBytes(dataSpaceMap) - dataSpaceMapBlocks := len(dataSpaceMap) / 64 - if len(dataSpaceMap)%64 > 0 { - dataSpaceMapBlocks++ - } - for i := 1; i < dataSpaceMapBlocks; i++ { - SSAT = append(SSAT, i+miniProperties) - } - SSAT = append(SSAT, -2) - miniProperties += dataSpaceMapBlocks - dataSpaceStream := c.writeStrongEncryptionDataSpaceStream() - storage.writeBytes(dataSpaceStream) - dataSpaceStreamBlocks := len(dataSpaceStream) / 64 - if len(dataSpaceStream)%64 > 0 { - dataSpaceStreamBlocks++ - } - for i := 1; i < dataSpaceStreamBlocks; i++ { - SSAT = append(SSAT, i+miniProperties) - } - SSAT = append(SSAT, -2) - miniProperties += dataSpaceStreamBlocks - primaryStream := make([]byte, 0x1C0) - primary := c.writePrimaryStream() - copy(primaryStream, primary) - storage.writeBytes(primaryStream) - primaryBlocks := len(primary) / 64 - if len(primary)%64 > 0 { - primaryBlocks++ - } - for i := 1; i < primaryBlocks; i++ { - SSAT = append(SSAT, i+miniProperties) - } - SSAT = append(SSAT, -2) - if len(SSAT) < 128 { - for i := len(SSAT); i < 128; i++ { - SSAT = append(SSAT, -1) +// put provides a function to add an entry to compound file by given entry name +// and raw bytes. +func (c *cfb) put(name string, content []byte) { + path := c.paths[0] + if len(path) <= len(name) && name[:len(path)] == path { + path = name + } else { + if len(path) > 0 && string(path[len(path)-1]) != "/" { + path += "/" + } + path = strings.ReplaceAll(path+name, "//", "/") + } + file := sector{name: path, typeID: 2, content: content, size: len(content)} + c.sectors = append(c.sectors, file) + c.paths = append(c.paths, path) +} + +// compare provides a function to compare object path, each set of sibling +// objects in one level of the containment hierarchy (all child objects under +// a storage object) is represented as a red-black tree. The parent object of +// this set of siblings will have a pointer to the top of this tree. +func (c *cfb) compare(left, right string) int { + L, R, i, j := strings.Split(left, "/"), strings.Split(right, "/"), 0, 0 + for Z := int(math.Min(float64(len(L)), float64(len(R)))); i < Z; i++ { + if j = len(L[i]) - len(R[i]); j != 0 { + return j + } + if L[i] != R[i] { + if L[i] < R[i] { + return -1 + } + return 1 } } - storage.position = 0 - return storage.stream, SSAT -} - -// writeRootEntry provides a function to write compound file root directory -// entry. The first entry in the first sector of the directory chain -// (also referred to as the first element of the directory array, or stream -// ID #0) is known as the root directory entry, and it is reserved for two -// purposes. First, it provides a root parent for all objects that are -// stationed at the root of the compound file. Second, its function is -// overloaded to store the size and starting sector for the mini stream. -func (c *cfb) writeRootEntry(customSectID int) []byte { - storage := cfb{stream: make([]byte, 128)} - storage.writeStrings("Root Entry") - storage.position = 0x40 - storage.writeUint16(0x16) - storage.writeBytes([]byte{5, 0}) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.writeUint32(1) - storage.position = 0x64 - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(customSectID) - storage.writeUint32(0x340) - return storage.stream -} - -// writeEncryptionInfo provides a function to write compound file -// writeEncryptionInfo stream. The writeEncryptionInfo stream contains -// detailed information that is used to initialize the cryptography used to -// encrypt the EncryptedPackage stream. -func (c *cfb) writeEncryptionInfo() []byte { - storage := cfb{stream: make([]byte, 128)} - storage.writeStrings("EncryptionInfo") - storage.position = 0x40 - storage.writeUint16(0x1E) - storage.writeBytes([]byte{2, 1}) - storage.writeUint32(0x03) - storage.writeUint32(0x02) - storage.writeUint32(-1) - storage.position = 0x64 - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0xF8) - return storage.stream -} - -// writeEncryptedPackage provides a function to write compound file -// writeEncryptedPackage stream. The writeEncryptedPackage stream is an -// encrypted stream of bytes containing the entire ECMA-376 source file in -// compressed form. -func (c *cfb) writeEncryptedPackage(propertyCount, size int) []byte { - storage := cfb{stream: make([]byte, 128)} - storage.writeStrings("EncryptedPackage") - storage.position = 0x40 - storage.writeUint16(0x22) - storage.writeBytes([]byte{2, 0}) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.position = 0x64 - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(propertyCount) - storage.writeUint32(size) - return storage.stream -} - -// writeDataSpaces provides a function to write compound file writeDataSpaces -// stream. The data spaces structure consists of a set of interrelated -// storages and streams in an OLE compound file. -func (c *cfb) writeDataSpaces() []byte { - storage := cfb{stream: make([]byte, 128)} - storage.writeUint16(0x06) - storage.position = 0x40 - storage.writeUint16(0x18) - storage.writeBytes([]byte{1, 0}) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.writeUint32(5) - storage.position = 0x64 - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - return storage.stream -} - -// writeVersion provides a function to write compound file version. The -// writeVersion structure specifies the version of a product or feature. It -// contains a major and a minor version number. -func (c *cfb) writeVersion() []byte { - storage := cfb{stream: make([]byte, 128)} - storage.writeStrings("Version") - storage.position = 0x40 - storage.writeUint16(0x10) - storage.writeBytes([]byte{2, 1}) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.position = 0x64 - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(4) - storage.writeUint32(76) - return storage.stream -} - -// writeDataSpaceMap provides a function to write compound file -// writeDataSpaceMap stream. The writeDataSpaceMap structure associates -// protected content with data space definitions. The data space definition, -// in turn, describes the series of transforms that MUST be applied to that -// protected content to restore it to its original form. By using a map to -// associate data space definitions with content, a single data space -// definition can be used to define the transforms applied to more than one -// piece of protected content. However, a given piece of protected content can -// be referenced only by a single data space definition. -func (c *cfb) writeDataSpaceMap() []byte { - storage := cfb{stream: make([]byte, 128)} - storage.writeStrings("DataSpaceMap") - storage.position = 0x40 - storage.writeUint16(0x1A) - storage.writeBytes([]byte{2, 1}) - storage.writeUint32(0x04) - storage.writeUint32(0x06) - storage.writeUint32(-1) - storage.position = 0x64 - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(6) - storage.writeUint32(112) - return storage.stream -} - -// writeDataSpaceInfo provides a function to write compound file -// writeDataSpaceInfo storage. The writeDataSpaceInfo is a storage containing -// the data space definitions used in the file. This storage must contain one -// or more streams, each of which contains a DataSpaceDefinition structure. -// The storage must contain exactly one stream for each DataSpaceMapEntry -// structure in the DataSpaceMap stream. The name of each stream must be equal -// to the DataSpaceName field of exactly one DataSpaceMapEntry structure -// contained in the DataSpaceMap stream. -func (c *cfb) writeDataSpaceInfo() []byte { - storage := cfb{stream: make([]byte, 128)} - storage.writeStrings("DataSpaceInfo") - storage.position = 0x40 - storage.writeUint16(0x1C) - storage.writeBytes([]byte{1, 1}) - storage.writeUint32(-1) - storage.writeUint32(8) - storage.writeUint32(7) - storage.position = 0x64 - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - return storage.stream -} - -// writeStrongEncryptionDataSpace provides a function to write compound file -// writeStrongEncryptionDataSpace stream. -func (c *cfb) writeStrongEncryptionDataSpace() []byte { - storage := cfb{stream: make([]byte, 128)} - storage.writeStrings("StrongEncryptionDataSpace") - storage.position = 0x40 - storage.writeUint16(0x34) - storage.writeBytes([]byte{2, 1}) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.position = 0x64 - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(8) - storage.writeUint32(64) - return storage.stream -} - -// writeTransformInfo provides a function to write compound file -// writeTransformInfo storage. writeTransformInfo is a storage containing -// definitions for the transforms used in the data space definitions stored in -// the DataSpaceInfo storage. The stream contains zero or more definitions for -// the possible transforms that can be applied to the data in content -// streams. -func (c *cfb) writeTransformInfo() []byte { - storage := cfb{stream: make([]byte, 128)} - storage.writeStrings("TransformInfo") - storage.position = 0x40 - storage.writeUint16(0x1C) - storage.writeBytes([]byte{1, 0}) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.writeUint32(9) - storage.position = 0x64 - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - return storage.stream + return len(L) - len(R) } -// writeStrongEncryptionTransform provides a function to write compound file -// writeStrongEncryptionTransform storage. -func (c *cfb) writeStrongEncryptionTransform() []byte { - storage := cfb{stream: make([]byte, 128)} - storage.writeStrings("StrongEncryptionTransform") - storage.position = 0x40 - storage.writeUint16(0x34) - storage.writeBytes([]byte{1}) - storage.writeBytes([]byte{1}) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.writeUint32(0x0A) - storage.position = 0x64 - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - return storage.stream +// prepare provides a function to prepare object before write stream. +func (c *cfb) prepare() { + type object struct { + path string + sector sector + } + var objects []object + for i := 0; i < len(c.paths); i++ { + if c.sectors[i].typeID == 0 { + continue + } + objects = append(objects, object{path: c.paths[i], sector: c.sectors[i]}) + } + sort.Slice(objects, func(i, j int) bool { + return c.compare(objects[i].path, objects[j].path) == 0 + }) + c.paths, c.sectors = []string{}, []sector{} + for i := 0; i < len(objects); i++ { + c.paths = append(c.paths, objects[i].path) + c.sectors = append(c.sectors, objects[i].sector) + } + for i := 0; i < len(objects); i++ { + sector, path := &c.sectors[i], c.paths[i] + sector.name, sector.color = filepath.Base(path), 1 + sector.L, sector.R, sector.C = -1, -1, -1 + sector.size, sector.start = len(sector.content), 0 + if len(sector.clsID) == 0 { + sector.clsID = headerCLSID + } + if i == 0 { + sector.C = -1 + if len(objects) > 1 { + sector.C = 1 + } + sector.size, sector.typeID = 0, 5 + } else { + if len(c.paths) > i+1 && filepath.Dir(c.paths[i+1]) == filepath.Dir(path) { + sector.R = i + 1 + } + sector.typeID = 2 + } + } } -// writePrimary provides a function to write compound file writePrimary stream. -func (c *cfb) writePrimary() []byte { - storage := cfb{stream: make([]byte, 128)} - storage.writeUint16(0x06) - storage.writeStrings("Primary") - storage.position = 0x40 - storage.writeUint16(0x12) - storage.writeBytes([]byte{2, 1}) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.position = 0x64 - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(9) - storage.writeUint32(208) - return storage.stream +// locate provides a function to locate sectors location and size of the +// compound file. +func (c *cfb) locate() []int { + var miniStreamSectorSize, FATSectorSize int + for i := 0; i < len(c.sectors); i++ { + sector := c.sectors[i] + if len(sector.content) == 0 { + continue + } + size := len(sector.content) + if size > 0 { + if size < 0x1000 { + miniStreamSectorSize += (size + 0x3F) >> 6 + } else { + FATSectorSize += (size + 0x01FF) >> 9 + } + } + } + directorySectors := (len(c.paths) + 3) >> 2 + miniStreamSectors := (miniStreamSectorSize + 7) >> 3 + miniFATSectors := (miniStreamSectorSize + 0x7F) >> 7 + sectors := miniStreamSectors + FATSectorSize + directorySectors + miniFATSectors + FATSectors := (sectors + 0x7F) >> 7 + DIFATSectors := 0 + if FATSectors > 109 { + DIFATSectors = int(math.Ceil((float64(FATSectors) - 109) / 0x7F)) + } + for ((sectors + FATSectors + DIFATSectors + 0x7F) >> 7) > FATSectors { + FATSectors++ + if FATSectors <= 109 { + DIFATSectors = 0 + } else { + DIFATSectors = int(math.Ceil((float64(FATSectors) - 109) / 0x7F)) + } + } + location := []int{1, DIFATSectors, FATSectors, miniFATSectors, directorySectors, FATSectorSize, miniStreamSectorSize, 0} + c.sectors[0].size = miniStreamSectorSize << 6 + c.sectors[0].start = location[0] + location[1] + location[2] + location[3] + location[4] + location[5] + location[7] = c.sectors[0].start + ((location[6] + 7) >> 3) + return location } -// writeNoneDir provides a function to write compound file writeNoneDir stream. -func (c *cfb) writeNoneDir() []byte { - storage := cfb{stream: make([]byte, 128)} - storage.position = 0x40 - storage.writeUint16(0x00) - storage.writeUint16(0x00) - storage.writeUint32(-1) - storage.writeUint32(-1) - storage.writeUint32(-1) - return storage.stream +// writeMSAT provides a function to write compound file master sector allocation +// table. +func (c *cfb) writeMSAT(location []int) { + var i, offset int + for i = 0; i < 109; i++ { + if i < location[2] { + c.writeUint32(location[1] + i) + } else { + c.writeUint32(-1) + } + } + if location[1] != 0 { + for offset = 0; offset < location[1]; offset++ { + for ; i < 236+offset*127; i++ { + if i < location[2] { + c.writeUint32(location[1] + i) + } else { + c.writeUint32(-1) + } + } + if offset == location[1]-1 { + c.writeUint32(endOfChain) + } else { + c.writeUint32(offset + 1) + } + } + } } // writeDirectoryEntry provides a function to write compound file directory @@ -1083,189 +843,175 @@ func (c *cfb) writeNoneDir() []byte { // within a compound file is represented by a single directory entry. The // space for the directory sectors that are holding the array is allocated // from the FAT. -func (c *cfb) writeDirectoryEntry(propertyCount, customSectID, size int) []byte { - var storage cfb - if size < 0 { - size = 0 - } - for _, entry := range [][]byte{ - c.writeRootEntry(customSectID), - c.writeEncryptionInfo(), - c.writeEncryptedPackage(propertyCount, size), - c.writeDataSpaces(), - c.writeVersion(), - c.writeDataSpaceMap(), - c.writeDataSpaceInfo(), - c.writeStrongEncryptionDataSpace(), - c.writeTransformInfo(), - c.writeStrongEncryptionTransform(), - c.writePrimary(), - c.writeNoneDir(), - } { - storage.writeBytes(entry) - } - return storage.stream -} - -// writeMSAT provides a function to write compound file master sector allocation -// table. -func (c *cfb) writeMSAT(MSATBlocks, SATBlocks int, MSAT []int) []int { - if MSATBlocks > 0 { - cnt, MSATIdx := MSATBlocks*128+109, 0 - for i := 0; i < cnt; i++ { - if i < SATBlocks { - bufferSize := i - 109 - if bufferSize > 0 && bufferSize%0x80 == 0 { - MSATIdx++ - MSAT = append(MSAT, MSATIdx) - } - MSAT = append(MSAT, i+MSATBlocks) - continue - } - MSAT = append(MSAT, -1) +func (c *cfb) writeDirectoryEntry(location []int) { + var sector sector + var j, sectorSize int + for i := 0; i < location[4]<<2; i++ { + var path string + if i < len(c.paths) { + path = c.paths[i] } - return MSAT - } - for i := 0; i < 109; i++ { - if i < SATBlocks { - MSAT = append(MSAT, i) + if i >= len(c.paths) || len(path) == 0 { + for j = 0; j < 17; j++ { + c.writeUint32(0) + } + for j = 0; j < 3; j++ { + c.writeUint32(-1) + } + for j = 0; j < 12; j++ { + c.writeUint32(0) + } continue } - MSAT = append(MSAT, -1) - } - return MSAT -} - -// writeSAT provides a function to write compound file sector allocation -// table. -func (c *cfb) writeSAT(MSATBlocks, SATBlocks, SSATBlocks, directoryBlocks, fileBlocks, streamBlocks int, SAT []int) (int, []int) { - var blocks int - if SATBlocks > 0 { - for i := 1; i <= MSATBlocks; i++ { - SAT = append(SAT, -4) + sector = c.sectors[i] + if i == 0 { + if sector.size > 0 { + sector.start = sector.start - 1 + } else { + sector.start = endOfChain + } } - blocks = MSATBlocks - for i := 1; i <= SATBlocks; i++ { - SAT = append(SAT, -3) + name := sector.name + sectorSize = 2 * (len(name) + 1) + c.writeStrings(name) + c.position += 64 - 2*(len(name)) + c.writeUint16(sectorSize) + c.writeBytes([]byte(string(rune(sector.typeID)))) + c.writeBytes([]byte(string(rune(sector.color)))) + c.writeUint32(sector.L) + c.writeUint32(sector.R) + c.writeUint32(sector.C) + if len(sector.clsID) == 0 { + for j = 0; j < 4; j++ { + c.writeUint32(0) + } + } else { + c.writeBytes(sector.clsID) } - blocks += SATBlocks - for i := 1; i < SSATBlocks; i++ { - SAT = append(SAT, i) + c.writeUint32(sector.state) + c.writeUint32(0) + c.writeUint32(0) + c.writeUint32(0) + c.writeUint32(0) + c.writeUint32(sector.start) + c.writeUint32(sector.size) + c.writeUint32(0) + } +} + +// writeSectorChains provides a function to write compound file sector chains. +func (c *cfb) writeSectorChains(location []int) sector { + var i, j, offset, sectorSize int + writeSectorChain := func(head, offset int) int { + for offset += head; i < offset-1; i++ { + c.writeUint32(i + 1) } - SAT = append(SAT, -2) - blocks += SSATBlocks - for i := 1; i < directoryBlocks; i++ { - SAT = append(SAT, i+blocks) + if head != 0 { + i++ + c.writeUint32(endOfChain) } - SAT = append(SAT, -2) - blocks += directoryBlocks - for i := 1; i < fileBlocks; i++ { - SAT = append(SAT, i+blocks) + return offset + } + for offset += location[1]; i < offset; i++ { + c.writeUint32(difSect) + } + for offset += location[2]; i < offset; i++ { + c.writeUint32(fatSect) + } + offset = writeSectorChain(location[3], offset) + offset = writeSectorChain(location[4], offset) + sector := c.sectors[0] + for ; j < len(c.sectors); j++ { + if sector = c.sectors[j]; len(sector.content) == 0 { + continue } - SAT = append(SAT, -2) - blocks += fileBlocks - for i := 1; i < streamBlocks; i++ { - SAT = append(SAT, i+blocks) + if sectorSize = len(sector.content); sectorSize < 0x1000 { + continue } - SAT = append(SAT, -2) + c.sectors[j].start = offset + offset = writeSectorChain((sectorSize+0x01FF)>>9, offset) } - return blocks, SAT -} - -// Writer provides a function to create compound file with given info stream -// and package stream. -// -// MSAT - The master sector allocation table -// SSAT - The short sector allocation table -// SAT - The sector allocation table -func (c *cfb) Writer(encryptionInfoBuffer, encryptedPackage []byte) []byte { - var ( - storage cfb - MSAT, SAT, SSAT []int - directoryBlocks, fileBlocks, SSATBlocks = 3, 2, 1 - size = int(math.Max(float64(len(encryptedPackage)), float64(packageEncryptionChunkSize))) - streamBlocks = len(encryptedPackage) / 0x200 - ) - if len(encryptedPackage)%0x200 > 0 { - streamBlocks++ - } - propertyBlocks := directoryBlocks + fileBlocks + SSATBlocks - blockSize := (streamBlocks + propertyBlocks) * 4 - SATBlocks := blockSize / 0x200 - if blockSize%0x200 > 0 { - SATBlocks++ - } - MSATBlocks, blocksChanged := 0, true - for blocksChanged { - var SATCap, MSATCap int - blocksChanged = false - blockSize = (streamBlocks + propertyBlocks + SATBlocks + MSATBlocks) * 4 - SATCap = blockSize / 0x200 - if blockSize%0x200 > 0 { - SATCap++ + writeSectorChain((location[6]+7)>>3, offset) + for c.position&0x1FF != 0 { + c.writeUint32(endOfChain) + } + i, offset = 0, 0 + for j = 0; j < len(c.sectors); j++ { + if sector = c.sectors[j]; len(sector.content) == 0 { + continue } - if SATCap > SATBlocks { - SATBlocks, blocksChanged = SATCap, true + if sectorSize = len(sector.content); sectorSize == 0 || sectorSize >= 0x1000 { continue } - if SATBlocks > 109 { - blockRemains := (SATBlocks - 109) * 4 - blockBuffer := blockRemains % 0x200 - MSATCap = blockRemains / 0x200 - if blockBuffer > 0 { - MSATCap++ - } - if blockBuffer+(4*MSATCap) > 0x200 { - MSATCap++ + sector.start = offset + offset = writeSectorChain((sectorSize+0x3F)>>6, offset) + } + for c.position&0x1FF != 0 { + c.writeUint32(endOfChain) + } + return sector +} + +// write provides a function to create compound file package stream. +func (c *cfb) write() []byte { + c.prepare() + location := c.locate() + c.stream = make([]byte, location[7]<<9) + var i, j int + for i = 0; i < 8; i++ { + c.writeBytes([]byte{oleIdentifier[i]}) + } + c.writeBytes(make([]byte, 16)) + c.writeUint16(0x003E) + c.writeUint16(0x0003) + c.writeUint16(0xFFFE) + c.writeUint16(0x0009) + c.writeUint16(0x0006) + c.writeBytes(make([]byte, 10)) + c.writeUint32(location[2]) + c.writeUint32(location[0] + location[1] + location[2] + location[3] - 1) + c.writeUint32(0) + c.writeUint32(1 << 12) + if location[3] != 0 { + c.writeUint32(location[0] + location[1] + location[2] - 1) + } else { + c.writeUint32(endOfChain) + } + c.writeUint32(location[3]) + if location[1] != 0 { + c.writeUint32(location[0] - 1) + } else { + c.writeUint32(endOfChain) + } + c.writeUint32(location[1]) + c.writeMSAT(location) + sector := c.writeSectorChains(location) + c.writeDirectoryEntry(location) + for i = 1; i < len(c.sectors); i++ { + sector = c.sectors[i] + if sector.size >= 0x1000 { + c.position = (sector.start + 1) << 9 + for j = 0; j < sector.size; j++ { + c.writeBytes([]byte{sector.content[j]}) } - if MSATCap > MSATBlocks { - MSATBlocks, blocksChanged = MSATCap, true + for ; j&0x1FF != 0; j++ { + c.writeBytes([]byte{0}) } } } - MSAT = c.writeMSAT(MSATBlocks, SATBlocks, MSAT) - blocks, SAT := c.writeSAT(MSATBlocks, SATBlocks, SSATBlocks, directoryBlocks, fileBlocks, streamBlocks, SAT) - 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) - storage.writeUint16(9) - storage.writeUint32(6) - storage.writeUint32(0) - storage.writeUint32(0) - storage.writeUint32(SATBlocks) - storage.writeUint32(MSATBlocks + SATBlocks + SSATBlocks) - storage.writeUint32(0) - storage.writeUint32(0x00001000) - storage.writeUint32(SATBlocks + MSATBlocks) - storage.writeUint32(SSATBlocks) - if MSATBlocks > 0 { - storage.writeUint32(0) - storage.writeUint32(MSATBlocks) - } else { - storage.writeUint32(-2) - storage.writeUint32(0) - } - for _, block := range MSAT { - storage.writeUint32(block) - } - for i := 0; i < SATBlocks*128; i++ { - if i < len(SAT) { - storage.writeUint32(SAT[i]) - continue + for i = 1; i < len(c.sectors); i++ { + sector = c.sectors[i] + if sector.size > 0 && sector.size < 0x1000 { + for j = 0; j < sector.size; j++ { + c.writeBytes([]byte{sector.content[j]}) + } + for ; j&0x3F != 0; j++ { + c.writeBytes([]byte{0}) + } } - storage.writeUint32(-1) } - fileStream, SSATStream := c.writeFileStream(encryptionInfoBuffer, SSAT) - for _, block := range SSATStream { - storage.writeUint32(block) + for c.position < len(c.stream) { + c.writeBytes([]byte{0}) } - directoryEntry := c.writeDirectoryEntry(blocks, blocks-fileBlocks, size) - storage.writeBytes(directoryEntry) - storage.writeBytes(fileStream) - storage.writeBytes(encryptedPackage) - return storage.stream + return c.stream } diff --git a/crypt_test.go b/crypt_test.go index d2fba35983..f7c465ed09 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -59,11 +59,6 @@ func TestEncryptionMechanism(t *testing.T) { assert.EqualError(t, err, ErrUnknownEncryptMechanism.Error()) } -func TestEncryptionWriteDirectoryEntry(t *testing.T) { - cfb := cfb{} - assert.Equal(t, 1536, len(cfb.writeDirectoryEntry(0, 0, -1))) -} - func TestHashing(t *testing.T) { assert.Equal(t, hashing("unsupportedHashAlgorithm", []byte{}), []byte(nil)) } From ab12307393461e7055f664d296a3a0e686eebb39 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 21 Aug 2022 01:09:32 +0800 Subject: [PATCH 091/213] This made library allowing insert EMZ and WMZ format image - Update dependencies module --- excelize.go | 6 +++--- go.mod | 4 ++-- go.sum | 8 ++++---- picture.go | 2 +- picture_test.go | 4 ++++ test/images/excel.emz | Bin 0 -> 2233 bytes test/images/excel.wmz | Bin 0 -> 1919 bytes xmlDrawing.go | 2 +- 8 files changed, 15 insertions(+), 11 deletions(-) create mode 100644 test/images/excel.emz create mode 100644 test/images/excel.wmz diff --git a/excelize.go b/excelize.go index f3b4381da2..f1269fef68 100644 --- a/excelize.go +++ b/excelize.go @@ -394,12 +394,12 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { } // UpdateLinkedValue fix linked values within a spreadsheet are not updating in -// Office Excel 2007 and 2010. This function will be remove value tag when met a +// Office Excel application. This function will be remove value tag when met a // cell have a linked value. Reference // https://social.technet.microsoft.com/Forums/office/en-US/e16bae1f-6a2c-4325-8013-e989a3479066/excel-2010-linked-cells-not-updating // -// Notice: after open XLSX file Excel will be update linked value and generate -// new value and will prompt save file or not. +// Notice: after opening generated workbook, Excel will update the linked value +// and generate a new value and will prompt to save the file or not. // // For example: // diff --git a/go.mod b/go.mod index 0fda81006d..b03e25465e 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,9 @@ require ( github.com/stretchr/testify v1.7.1 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-20220722155217-630584e8d5aa + golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 - golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced + golang.org/x/net v0.0.0-20220812174116-3211cb980234 golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index a79ea1f2e4..9512add09e 100644 --- a/go.sum +++ b/go.sum @@ -17,13 +17,13 @@ github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj0 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-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUHW7cJMmx3TGZOrnyYaNQ6c= +golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/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-20220809184613-07c6da5e1ced h1:3dYNDff0VT5xj+mbj2XucFst9WKk6PdGOrb9n+SbIvw= -golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E= +golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 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= diff --git a/picture.go b/picture.go index 84c7731681..d087c61e28 100644 --- a/picture.go +++ b/picture.go @@ -363,7 +363,7 @@ func (f *File) addMedia(file []byte, ext string) string { // setContentTypePartImageExtensions provides a function to set the content // type for relationship parts and the Main Document part. func (f *File) setContentTypePartImageExtensions() { - imageTypes := map[string]string{"jpeg": "image/", "png": "image/", "gif": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-"} + imageTypes := map[string]string{"jpeg": "image/", "png": "image/", "gif": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-", "emz": "image/x-", "wmz": "image/x-"} content := f.contentTypesReader() content.Lock() defer content.Unlock() diff --git a/picture_test.go b/picture_test.go index 3ac1afb69c..37ccdc560b 100644 --- a/picture_test.go +++ b/picture_test.go @@ -98,8 +98,12 @@ func TestAddPictureErrors(t *testing.T) { decodeConfig := func(r io.Reader) (image.Config, error) { return image.Config{Height: 100, Width: 90}, nil } image.RegisterFormat("emf", "", decode, decodeConfig) image.RegisterFormat("wmf", "", decode, decodeConfig) + image.RegisterFormat("emz", "", decode, decodeConfig) + image.RegisterFormat("wmz", "", decode, decodeConfig) assert.NoError(t, f.AddPicture("Sheet1", "Q1", filepath.Join("test", "images", "excel.emf"), "")) assert.NoError(t, f.AddPicture("Sheet1", "Q7", filepath.Join("test", "images", "excel.wmf"), "")) + assert.NoError(t, f.AddPicture("Sheet1", "Q13", filepath.Join("test", "images", "excel.emz"), "")) + assert.NoError(t, f.AddPicture("Sheet1", "Q19", filepath.Join("test", "images", "excel.wmz"), "")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx"))) assert.NoError(t, f.Close()) } diff --git a/test/images/excel.emz b/test/images/excel.emz new file mode 100644 index 0000000000000000000000000000000000000000..bc9480153a46787e76fbbd2521b8e1e73e3fc1d9 GIT binary patch literal 2233 zcmV;q2uAlGiwFpBF#loz|7mSuXJsyBZDs(ynq6!Z*A>U_cxOt??5=m$_yZ6QhOBVN z#+Wz`#@`lQi7U?R0*Yx6g7&fv{fJSoIdm=^s!G7 zLXlcU$wMh3sG@3;My-eGIrpA>=gxR`c4v3Z==?Q1Gk33l=brzaS?i9$7~2cFY%;d6 z;JO8ivF|yq`{_l-F1_QrZ@&v1z?2W6PIB?HIJbd3GZf$YF>D}AR4v;ovf{o>X+yM^!b+TVK;bU(EZ zZdS-S{rmWxVaj1OaoF&s^)Noja~yOP@?H(cy>NaH?AzenHFJ&Qsi>*sTsI$bZh(3$ zPa8o6s{|^7wkp}ydZiKK7^9ruK4)<4KXuDRg^f#s^6CD~0bgEtzDFCQupH3~nBy7B z@xKcq$9o}1?E8kfupo6Ebj`d4Vvu7-$+q%JUDiEH1vvJUQL?Rt1*ux{l|IOvnOx)p znJ?!4IB10ON<%&=$ZG@avEA25f?wot&CIKuqQ<%F_1ReCY{1`5l9w(J&vH0k2Iq6I z@8TRu$5TdATFEIRSCe6-ohGZrNkC3kS*786651wf@|zt0_HS;E9RJ`rSdedCn^OkO zF&u|Uno|j=gQuY~8q+kcWP&Vf6f^?LE7dcn(#M8k)f#imP>y>k$28@L-fQ7GW*kph zP>$r~Zu#WZuAX|e77MZJl>p{(Ef@6dC>&#s+bEYN81p7;0=zT<9@RCas3!(a#I^()dlWZ%6sQ2m-Unjkh!aQ8+i}N`*JZXPfc9XbGi?8e~v&~7%6iUzOT#c zzpgqyF&wLoF5Uky_Y&xTrnnzjbpOHc1@9*e?jeTHrx|`X+!?EO39EeV z#;JA&%o9&TnnrjXoQPWJ=Uq)$L#(B!sW&(4sa^AqeC#~V{&BiDmh+gSN%(Apyz14fo?0y1+7+u7M&Owxe=jApXO{mriIe1$kL7qzRb7}HU&XIlL^+9SkNPWrAvM=m=h}sNO zUsyBO9ELg$tGiAkU9Lqco1P2Gg1k9tOR?%Il-a63=&eOml7N6GR z7d$Q$4~clAIQPa*i2nU8ecw)r*fdMgJDbFK9OK@7G9IHSJZ2a6V%!7qsD0Nqdv&k9 zrZ08p0I>Y^>W4eet{MAb%P7PTFh)-BcphW;`SkE}7(>reY<-5u@$}&F6vi=(Ek}7A zNj)IqaBARia^Pu<`ymeTm`nDHIGDWepnc!>F*YBt`<}A925io9^vzx4Co^LwTL5QF z;^;((6HO#=sw48@NFo>@&N6-~gCTIDi3E-!dM1iU;rNISl_laV6Z4q~fuo27jv_h{ zMzm;vd_+fsM2Hh0@|R4ooX#MQB03pPv_vNAmqeUp@^ofW;mmsj6x&3RB?~9gN+wv& zWH8DQDV)ZdXmQCn%hZpuQ)jXsPM~CEOf=F^6G6!oi--uK8cG&6?;}jaS*BmeLMR*` z(Gr{J0gXsw2@q9jq6+gqn#f-=&a&-ncAJJXT}{cNO;l;#hY%^29FJ`10EH6OUb6BJ z4<8HSR7Dh8vML@P(I(<7Ge6GFcsRLW$zBL4*<^JktLot)i4+URi6UBh6LFU9f~5>m z4JE5>-V;kJ#EhS)+LG06A~o*?%ZsfT0ix)Vg_)?zl9e+NXW1cGBu;e6VwS$4j(0wRdha1))bU?O5^Bb*ST+h-z=r7a#&6()*XGR{(b zxeX#nblXj&v0(UdqD@p|$;v-GILj`@5;{OJN>-PN1j|p`12_#VS@kATEFuC#ktK^J zs$t%9mXcyYoOncynfIJXvWU^|2~!l;gD$xI|! zMCdrNOw^EvhhTZ7L*YacEv<<-%R{epK!J_ODB(x#RGIZ zO~hH|-+lP&=R0d9s#LP-Of+^p+xH0k_Wt~jLU_ZkTJGo6t_`3r+uoex<%J zH!I!L^qG0LrXJD;pRU6>p}U>G!*`^DYm0H#s^xo{{eOPZH?B=Adab%gduI{VY2f4@+?+b2};iD|mEuo2q!fa=Mqn)ed1y?igRncjcvwY;@r`Cg)}-kZetL$|8; zE58BP4pKj#yWSctZa*i0Wvpr4Bzu<4vHk3D86Q{L^m=sC=TfQUd&jq(KI8RofxZJm zfA57{?tq*Q;F>bEHR#$?v_@T`zY}ba{mii4Y#)vPa@y;j(8p4r&uP$#Kpy`GOub?+ H_$&YbR>)@k literal 0 HcmV?d00001 diff --git a/test/images/excel.wmz b/test/images/excel.wmz new file mode 100644 index 0000000000000000000000000000000000000000..d6089680745e83186790e7a4ebd04720387a85d2 GIT binary patch literal 1919 zcmV-_2Y~n=iwFqcF#loz|7mSuXJsyTZDs)N-CIl)R~QHILG6QCz=d^zMvJy-qiLHO zMWNmWMHIK&loBA7#)=58Qk1I{5J7GQLI}3>p%|m7ma1v=!JtXohZfT+0j#y58roo+ zK445F@kNc`_B(TShnX{%on3bK4Cy)flbyZco*(D?&dk2}$KcOja{S2R9Nb6z(WcB0 zZZ5~o&X4D~5H2|$JvIk^W`%_Cw{L~S@V9e9!nhaU)-(&f0v(0Ua=`CF??^N)<_0)) z4-NP2QvTmT_t9^T;UC*pz;V~Y@sE2r*guYitA)E)u3Sk^Pk;9885}=& z@W5`jswn}@v+Tjd-(8SSy`D)Hl99x8vH3te@sqJW@KcfrKJg=Ze`vw)MGosE64LvL?yPEO9FM~^6eet(U-ckdPz6%`g14h#%9d6vw~%;x6io}M13 z$l?0+>!qcoO-)UMgM&`D0~~a8bkx??K7an4>LmOyE?l^9=FFK(mo9a8cf(y+1_cEL zBO@af6%|gA1I+j0#fyD?eO+B$PLTtw6EHk~{yf!5_+ix4)WAv@9UXNz9B}vS*|R54 zo}8GN$jZuk^5hA2fJL%%=T10=S?A~H!(U%;fN}r+eR%fv?b}r6F|7f`4pk2Y4pYN3 zg~g9oKCFkKp&{%5b1W$-!Eb46YrAvjj@SVPyk*y}UE|~9bfz)w0acKfmxqsAT3Vnw zpkY3K{21<@K7IPssZ-eD#EBE|6n@!_8#mx_!T|>Klgi4<8QLh$ojcdw-j0v2UcH)| zn_FI9j_>yO_e1~04tw|Rg#-LJEFrvI5)M#1#l^+YYJ&Os{WYqpsxDu?jE~30#$a~P zAMjl$77)01?;bn|#Ug%JXJ;pNxOwv?aqK~ptFbC$q9GC-hU=GY-hB~~NYVlW?gFz^!SF9hOaF_SySxF>LNae!rwj=hH?-k zp^(qVVevQxVXF#4R~&kRSgY?#A)HLGU8q^&UtIUZ4Mdj4O z@VaSemWi^4s7&ZP;A0gHfx^E{LtZA-9g>bjBvnUDQ4nRK(lpljwP`?^7{UQZL&JwM zQQBKIng(@;%{37afkL7Y;HD8T(lCevq2YP~uR&Z^c-&iw*Q;ts%Y?eamZK3{L=>Bq z%7mm)ZyLla^)?MdI7IS^P+S;TLw>QTSSIq9=|3Sij02+3&>$vA?ybBA@fu<`(QX>l z9g=G!lcy*m$ubdX$SK5oEAgz}6LPnPAsl!LEe)Q6bdc_?q=T0yxr z^2H$AO~?hKeHBk=(06EzA}HhiNqqUc(3uIO2t&vuSvHLaw1f-=QfAg`m*UG<-NAS16OCMsawjDcXjj zPeXHW)z;Ad>dy!cIARKqO~dOl@$}UneTU{~fkIOwuuX%ygS|N#BG&MtO#Ih04C3IT z@J++h3Avu8LEYiQ&NuAMF%k`3W#Z4KkyJ^)Pk!Ckmf8LaPvL`xr!vu@kz8X**hBxk zdSY_ov)#P~uQNN7>EC))13=EGo6mx?P%WwL<0#NjWNx1+ulq zvh|21vBVOWZq7a)izEM#b=Tqo9pUWv*U=w;`0X3W_lmjY%6GSV#CfkFcE^!uSE|(# zg_9~v%3*6tS=8nO)+C43AP&Bbtd`h}PZ#){v||g>8_~fB9WSXN7~f&|-(Q43qF Date: Wed, 24 Aug 2022 00:00:47 +0800 Subject: [PATCH 092/213] This closes #1290 and closes #1328 - Add new smooth field in chart format parameter, support specify if smooth line chart - Fix decimal number format round issue with build-in number format --- cell_test.go | 8 ++++++++ drawing.go | 4 +--- styles.go | 2 +- xmlChart.go | 7 ++++--- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/cell_test.go b/cell_test.go index fb1e8ef585..2a09a9dc9e 100644 --- a/cell_test.go +++ b/cell_test.go @@ -699,6 +699,14 @@ func TestFormattedValue2(t *testing.T) { }) v = f.formattedValue(1, "43528", false) assert.Equal(t, "43528", v) + + // formatted decimal value with build-in number format ID + styleID, err := f.NewStyle(&Style{ + NumFmt: 1, + }) + assert.NoError(t, err) + v = f.formattedValue(styleID, "310.56", false) + assert.Equal(t, "311", v) } func TestSharedStringsError(t *testing.T) { diff --git a/drawing.go b/drawing.go index 5015d265bf..59b6d2a9c8 100644 --- a/drawing.go +++ b/drawing.go @@ -543,9 +543,6 @@ func (f *File) drawLineChart(formatSet *formatChart) *cPlotArea { }, Ser: f.drawChartSeries(formatSet), DLbls: f.drawChartDLbls(formatSet), - Smooth: &attrValBool{ - Val: boolPtr(false), - }, AxID: []*attrValInt{ {Val: intPtr(754001152)}, {Val: intPtr(753999904)}, @@ -757,6 +754,7 @@ func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { DLbls: f.drawChartSeriesDLbls(formatSet), InvertIfNegative: &attrValBool{Val: boolPtr(false)}, Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), + Smooth: &attrValBool{Val: boolPtr(formatSet.Series[k].Line.Smooth)}, Val: f.drawChartSeriesVal(formatSet.Series[k], formatSet), XVal: f.drawChartSeriesXVal(formatSet.Series[k], formatSet), YVal: f.drawChartSeriesYVal(formatSet.Series[k], formatSet), diff --git a/styles.go b/styles.go index 8eb0587884..2986f16d1d 100644 --- a/styles.go +++ b/styles.go @@ -852,7 +852,7 @@ func formatToInt(v, format string, date1904 bool) string { if err != nil { return v } - return fmt.Sprintf("%d", int64(f)) + return fmt.Sprintf("%d", int64(math.Round(f))) } // formatToFloat provides a function to convert original string to float diff --git a/xmlChart.go b/xmlChart.go index b6ee3cd81a..dcd33e4a4b 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -620,9 +620,10 @@ type formatChartSeries struct { Categories string `json:"categories"` Values string `json:"values"` Line struct { - None bool `json:"none"` - Color string `json:"color"` - Width float64 `json:"width"` + None bool `json:"none"` + Color string `json:"color"` + Smooth bool `json:"smooth"` + Width float64 `json:"width"` } `json:"line"` Marker struct { Symbol string `json:"symbol"` From 0e9378fec2ab4ba60ed284db4383df86555076d1 Mon Sep 17 00:00:00 2001 From: Cooper de Nicola Date: Wed, 24 Aug 2022 18:34:29 -0700 Subject: [PATCH 093/213] This closes #1247, add new function `SetSheetCol` for set worksheet column values (#1320) Signed-off-by: cdenicola Co-authored-by: cdenicola --- cell.go | 24 +++++++++++++++++++++--- excelize_test.go | 17 +++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/cell.go b/cell.go index 214f5c6f89..dd6b1695f1 100644 --- a/cell.go +++ b/cell.go @@ -1052,20 +1052,38 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { // // err := f.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { + return f.setSheetCells(sheet, axis, slice, rows) +} + +// SetSheetCol writes an array to column by given worksheet name, starting +// coordinate and a pointer to array type 'slice'. For example, writes an +// array to column B start with the cell B6 on Sheet1: +// +// err := f.SetSheetCol("Sheet1", "B6", &[]interface{}{"1", nil, 2}) +func (f *File) SetSheetCol(sheet, axis string, slice interface{}) error { + return f.setSheetCells(sheet, axis, slice, columns) +} + +// setSheetCells provides a function to set worksheet cells value. +func (f *File) setSheetCells(sheet, axis string, slice interface{}, dir adjustDirection) error { col, row, err := CellNameToCoordinates(axis) if err != nil { return err } - // Make sure 'slice' is a Ptr to Slice v := reflect.ValueOf(slice) if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Slice { return ErrParameterInvalid } v = v.Elem() - for i := 0; i < v.Len(); i++ { - cell, err := CoordinatesToCellName(col+i, row) + var cell string + var err error + if dir == rows { + cell, err = CoordinatesToCellName(col+i, row) + } else { + cell, err = CoordinatesToCellName(col, row+i) + } // Error should never happen here. But keep checking to early detect regressions // if it will be introduced in the future. if err != nil { diff --git a/excelize_test.go b/excelize_test.go index f1b9903cbb..5db658a215 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1123,6 +1123,23 @@ func TestSharedStrings(t *testing.T) { assert.NoError(t, f.Close()) } +func TestSetSheetCol(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + assert.NoError(t, f.SetSheetCol("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()})) + + assert.EqualError(t, f.SetSheetCol("Sheet1", "", &[]interface{}{"cell", nil, 2}), + newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) + + assert.EqualError(t, f.SetSheetCol("Sheet1", "B27", []interface{}{}), ErrParameterInvalid.Error()) + assert.EqualError(t, f.SetSheetCol("Sheet1", "B27", &f), ErrParameterInvalid.Error()) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetCol.xlsx"))) + assert.NoError(t, f.Close()) +} + func TestSetSheetRow(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { From f8667386dcde788d8232b652ac85a138c0d20bf3 Mon Sep 17 00:00:00 2001 From: chenliu1993 <13630583107@163.com> Date: Sat, 27 Aug 2022 00:45:46 +0800 Subject: [PATCH 094/213] This closes #827, add new functions `GetDataValidations` and `GetConditionalFormats` (#1315) Signed-off-by: chenliu1993 <13630583107@163.com> --- datavalidation.go | 12 +++ datavalidation_test.go | 31 +++++++ excelize_test.go | 10 +-- styles.go | 187 +++++++++++++++++++++++++++++++++++++++-- styles_test.go | 27 ++++++ xmlWorksheet.go | 8 +- 6 files changed, 258 insertions(+), 17 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 0cad1b8be9..3d82f7c57c 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -259,6 +259,18 @@ func (f *File) AddDataValidation(sheet string, dv *DataValidation) error { return err } +// GetDataValidations returns data validations list by given worksheet name. +func (f *File) GetDataValidations(sheet string) ([]*DataValidation, error) { + ws, err := f.workSheetReader(sheet) + if err != nil { + return nil, err + } + if ws.DataValidations == nil || len(ws.DataValidations.DataValidation) == 0 { + return nil, err + } + return ws.DataValidations.DataValidation, err +} + // DeleteDataValidation delete data validation by given worksheet name and // reference sequence. All data validations in the worksheet will be deleted // if not specify reference sequence parameter. diff --git a/datavalidation_test.go b/datavalidation_test.go index d9e060a54e..88625d10a7 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -32,6 +32,11 @@ func TestDataValidation(t *testing.T) { dvRange.SetError(DataValidationErrorStyleWarning, "error title", "error body") dvRange.SetError(DataValidationErrorStyleInformation, "error title", "error body") assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + + dataValidations, err := f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, len(dataValidations), 1) + assert.NoError(t, f.SaveAs(resultFile)) dvRange = NewDataValidation(true) @@ -39,6 +44,11 @@ func TestDataValidation(t *testing.T) { assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) dvRange.SetInput("input title", "input body") assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + + dataValidations, err = f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, len(dataValidations), 2) + assert.NoError(t, f.SaveAs(resultFile)) f.NewSheet("Sheet2") @@ -49,6 +59,12 @@ func TestDataValidation(t *testing.T) { assert.NoError(t, dvRange.SetRange("INDIRECT($A$2)", "INDIRECT($A$3)", DataValidationTypeWhole, DataValidationOperatorBetween)) dvRange.SetError(DataValidationErrorStyleStop, "error title", "error body") assert.NoError(t, f.AddDataValidation("Sheet2", dvRange)) + dataValidations, err = f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, len(dataValidations), 2) + dataValidations, err = f.GetDataValidations("Sheet2") + assert.NoError(t, err) + assert.Equal(t, len(dataValidations), 1) dvRange = NewDataValidation(true) dvRange.Sqref = "A5:B6" @@ -67,7 +83,22 @@ func TestDataValidation(t *testing.T) { } assert.Equal(t, `"A<,B>,C"",D ,E',F"`, dvRange.Formula1) assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + + dataValidations, err = f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, len(dataValidations), 3) + + // Test get data validation on no exists worksheet + _, err = f.GetDataValidations("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") + assert.NoError(t, f.SaveAs(resultFile)) + + // Test get data validation on a worksheet without data validation settings + f = NewFile() + dataValidations, err = f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, []*DataValidation(nil), dataValidations) } func TestDataValidationError(t *testing.T) { diff --git a/excelize_test.go b/excelize_test.go index 5db658a215..eac218f2c2 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1041,15 +1041,15 @@ func TestConditionalFormat(t *testing.T) { assert.NoError(t, f.SetConditionalFormat(sheet1, "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`)) // Color scales: 3 color. assert.NoError(t, f.SetConditionalFormat(sheet1, "B1:B10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`)) - // Hightlight cells rules: between... + // Highlight cells rules: between... assert.NoError(t, f.SetConditionalFormat(sheet1, "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1))) - // Hightlight cells rules: Greater Than... + // Highlight cells rules: Greater Than... assert.NoError(t, f.SetConditionalFormat(sheet1, "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3))) - // Hightlight cells rules: Equal To... + // Highlight cells rules: Equal To... assert.NoError(t, f.SetConditionalFormat(sheet1, "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3))) - // Hightlight cells rules: Not Equal To... + // Highlight cells rules: Not Equal To... assert.NoError(t, f.SetConditionalFormat(sheet1, "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2))) - // Hightlight cells rules: Duplicate Values... + // Highlight cells rules: Duplicate Values... assert.NoError(t, f.SetConditionalFormat(sheet1, "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2))) // Top/Bottom rules: Top 10%. assert.NoError(t, f.SetConditionalFormat(sheet1, "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1))) diff --git a/styles.go b/styles.go index 2986f16d1d..c770360492 100644 --- a/styles.go +++ b/styles.go @@ -844,6 +844,31 @@ var criteriaType = map[string]string{ "continue month": "continueMonth", } +// operatorType defined the list of valid operator types. +var operatorType = map[string]string{ + "lastMonth": "last month", + "between": "between", + "notEqual": "not equal to", + "greaterThan": "greater than", + "lessThanOrEqual": "less than or equal to", + "today": "today", + "equal": "equal to", + "notContains": "not containing", + "thisWeek": "this week", + "endsWith": "ends with", + "yesterday": "yesterday", + "lessThan": "less than", + "beginsWith": "begins with", + "last7Days": "last 7 days", + "thisMonth": "this month", + "containsText": "containing", + "lastWeek": "last week", + "continueWeek": "continue week", + "continueMonth": "continue month", + "notBetween": "not between", + "greaterThanOrEqual": "greater than or equal to", +} + // formatToInt provides a function to convert original string to integer // format as string type by given built-in number formats code and cell // string. @@ -2726,7 +2751,7 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // type: minimum - The minimum parameter is used to set the lower limiting value // when the criteria is either "between" or "not between". // -// // Hightlight cells rules: between... +// // Highlight cells rules: between... // f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format)) // // type: maximum - The maximum parameter is used to set the upper limiting value @@ -2744,12 +2769,12 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // // type: duplicate - The duplicate type is used to highlight duplicate cells in a range: // -// // Hightlight cells rules: Duplicate Values... +// // Highlight cells rules: Duplicate Values... // f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format)) // // type: unique - The unique type is used to highlight unique cells in a range: // -// // Hightlight cells rules: Not Equal To... +// // Highlight cells rules: Not Equal To... // f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format)) // // type: top - The top type is used to specify the top n values by number or percentage in a range: @@ -2837,7 +2862,7 @@ func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { "2_color_scale": drawCondFmtColorScale, "3_color_scale": drawCondFmtColorScale, "dataBar": drawCondFmtDataBar, - "expression": drawConfFmtExp, + "expression": drawCondFmtExp, } ws, err := f.workSheetReader(sheet) @@ -2854,9 +2879,9 @@ func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { // Check for valid criteria types. ct, ok = criteriaType[v.Criteria] if ok || vt == "expression" { - drawfunc, ok := drawContFmtFunc[vt] + drawFunc, ok := drawContFmtFunc[vt] if ok { - cfRule = append(cfRule, drawfunc(p, ct, v)) + cfRule = append(cfRule, drawFunc(p, ct, v)) } } } @@ -2869,6 +2894,152 @@ func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { return err } +// extractCondFmtCellIs provides a function to extract conditional format +// settings for cell value (include between, not between, equal, not equal, +// greater than and less than) by given conditional formatting rule. +func extractCondFmtCellIs(c *xlsxCfRule) *formatConditional { + format := formatConditional{Type: "cell", Criteria: operatorType[c.Operator], Format: *c.DxfID} + if len(c.Formula) == 2 { + format.Minimum, format.Maximum = c.Formula[0], c.Formula[1] + return &format + } + format.Value = c.Formula[0] + return &format +} + +// extractCondFmtTop10 provides a function to extract conditional format +// settings for top N (default is top 10) by given conditional formatting +// rule. +func extractCondFmtTop10(c *xlsxCfRule) *formatConditional { + format := formatConditional{ + Type: "top", + Criteria: "=", + Format: *c.DxfID, + Percent: c.Percent, + Value: strconv.Itoa(c.Rank), + } + if c.Bottom { + format.Type = "bottom" + } + return &format +} + +// extractCondFmtAboveAverage provides a function to extract conditional format +// settings for above average and below average by given conditional formatting +// rule. +func extractCondFmtAboveAverage(c *xlsxCfRule) *formatConditional { + return &formatConditional{ + Type: "average", + Criteria: "=", + Format: *c.DxfID, + AboveAverage: *c.AboveAverage, + } +} + +// extractCondFmtDuplicateUniqueValues provides a function to extract +// conditional format settings for duplicate and unique values by given +// conditional formatting rule. +func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule) *formatConditional { + return &formatConditional{ + Type: map[string]string{ + "duplicateValues": "duplicate", + "uniqueValues": "unique", + }[c.Type], + Criteria: "=", + Format: *c.DxfID, + } +} + +// extractCondFmtColorScale provides a function to extract conditional format +// settings for color scale (include 2 color scale and 3 color scale) by given +// conditional formatting rule. +func extractCondFmtColorScale(c *xlsxCfRule) *formatConditional { + var format formatConditional + format.Type, format.Criteria = "2_color_scale", "=" + values := len(c.ColorScale.Cfvo) + colors := len(c.ColorScale.Color) + if colors > 1 && values > 1 { + format.MinType = c.ColorScale.Cfvo[0].Type + if c.ColorScale.Cfvo[0].Val != "0" { + format.MinValue = c.ColorScale.Cfvo[0].Val + } + format.MinColor = "#" + strings.TrimPrefix(strings.ToUpper(c.ColorScale.Color[0].RGB), "FF") + format.MaxType = c.ColorScale.Cfvo[1].Type + if c.ColorScale.Cfvo[1].Val != "0" { + format.MaxValue = c.ColorScale.Cfvo[1].Val + } + format.MaxColor = "#" + strings.TrimPrefix(strings.ToUpper(c.ColorScale.Color[1].RGB), "FF") + } + if colors == 3 { + format.Type = "3_color_scale" + format.MidType = c.ColorScale.Cfvo[1].Type + if c.ColorScale.Cfvo[1].Val != "0" { + format.MidValue = c.ColorScale.Cfvo[1].Val + } + format.MidColor = "#" + strings.TrimPrefix(strings.ToUpper(c.ColorScale.Color[1].RGB), "FF") + format.MaxType = c.ColorScale.Cfvo[2].Type + if c.ColorScale.Cfvo[2].Val != "0" { + format.MaxValue = c.ColorScale.Cfvo[2].Val + } + format.MaxColor = "#" + strings.TrimPrefix(strings.ToUpper(c.ColorScale.Color[2].RGB), "FF") + } + return &format +} + +// extractCondFmtDataBar provides a function to extract conditional format +// settings for data bar by given conditional formatting rule. +func extractCondFmtDataBar(c *xlsxCfRule) *formatConditional { + format := formatConditional{Type: "data_bar", Criteria: "="} + if c.DataBar != nil { + format.MinType = c.DataBar.Cfvo[0].Type + format.MaxType = c.DataBar.Cfvo[1].Type + format.BarColor = "#" + strings.TrimPrefix(strings.ToUpper(c.DataBar.Color[0].RGB), "FF") + } + return &format +} + +// extractCondFmtExp provides a function to extract conditional format settings +// for expression by given conditional formatting rule. +func extractCondFmtExp(c *xlsxCfRule) *formatConditional { + format := formatConditional{Type: "formula", Format: *c.DxfID} + if len(c.Formula) > 0 { + format.Criteria = c.Formula[0] + } + return &format +} + +// GetConditionalFormats returns conditional format settings by given worksheet +// name. +func (f *File) GetConditionalFormats(sheet string) (map[string]string, error) { + extractContFmtFunc := map[string]func(c *xlsxCfRule) *formatConditional{ + "cellIs": extractCondFmtCellIs, + "top10": extractCondFmtTop10, + "aboveAverage": extractCondFmtAboveAverage, + "duplicateValues": extractCondFmtDuplicateUniqueValues, + "uniqueValues": extractCondFmtDuplicateUniqueValues, + "colorScale": extractCondFmtColorScale, + "dataBar": extractCondFmtDataBar, + "expression": extractCondFmtExp, + } + + conditionalFormats := make(map[string]string) + ws, err := f.workSheetReader(sheet) + if err != nil { + return conditionalFormats, err + } + for _, cf := range ws.ConditionalFormatting { + var format []*formatConditional + for _, cr := range cf.CfRule { + if extractFunc, ok := extractContFmtFunc[cr.Type]; ok { + format = append(format, extractFunc(cr)) + } + } + formatSet, _ := json.Marshal(format) + conditionalFormats[cf.SQRef] = string(formatSet) + } + return conditionalFormats, err +} + // UnsetConditionalFormat provides a function to unset the conditional format // by given worksheet name and range. func (f *File) UnsetConditionalFormat(sheet, area string) error { @@ -3001,9 +3172,9 @@ func drawCondFmtDataBar(p int, ct string, format *formatConditional) *xlsxCfRule } } -// drawConfFmtExp provides a function to create conditional formatting rule +// drawCondFmtExp provides a function to create conditional formatting rule // for expression by given priority, criteria type and format settings. -func drawConfFmtExp(p int, ct string, format *formatConditional) *xlsxCfRule { +func drawCondFmtExp(p int, ct string, format *formatConditional) *xlsxCfRule { return &xlsxCfRule{ Priority: p + 1, Type: validType[format.Type], diff --git a/styles_test.go b/styles_test.go index 156b4e33b1..96cc2adb92 100644 --- a/styles_test.go +++ b/styles_test.go @@ -175,6 +175,33 @@ func TestSetConditionalFormat(t *testing.T) { } } +func TestGetConditionalFormats(t *testing.T) { + for _, format := range []string{ + `[{"type":"cell","format":1,"criteria":"greater than","value":"6"}]`, + `[{"type":"cell","format":1,"criteria":"between","minimum":"6","maximum":"8"}]`, + `[{"type":"top","format":1,"criteria":"=","value":"6"}]`, + `[{"type":"bottom","format":1,"criteria":"=","value":"6"}]`, + `[{"type":"average","above_average":true,"format":1,"criteria":"="}]`, + `[{"type":"duplicate","format":1,"criteria":"="}]`, + `[{"type":"unique","format":1,"criteria":"="}]`, + `[{"type":"3_color_scale","criteria":"=","min_type":"num","mid_type":"num","max_type":"num","min_value":"-10","mid_value":"50","max_value":"10","min_color":"#FF0000","mid_color":"#00FF00","max_color":"#0000FF"}]`, + `[{"type":"2_color_scale","criteria":"=","min_type":"num","max_type":"num","min_color":"#FF0000","max_color":"#0000FF"}]`, + `[{"type":"data_bar","criteria":"=","min_type":"min","max_type":"max","bar_color":"#638EC6"}]`, + `[{"type":"formula","format":1,"criteria":"="}]`, + } { + f := NewFile() + err := f.SetConditionalFormat("Sheet1", "A1:A2", format) + assert.NoError(t, err) + formatSet, err := f.GetConditionalFormats("Sheet1") + assert.NoError(t, err) + assert.Equal(t, format, formatSet["A1:A2"]) + } + // Test get conditional formats on no exists worksheet + f := NewFile() + _, err := f.GetConditionalFormats("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") +} + func TestUnsetConditionalFormat(t *testing.T) { f := NewFile() assert.NoError(t, f.SetCellValue("Sheet1", "A1", 7)) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 3b9caac533..af7c4f3be3 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -826,10 +826,10 @@ type formatPanes struct { // formatConditional directly maps the conditional format settings of the cells. type formatConditional struct { Type string `json:"type"` - AboveAverage bool `json:"above_average"` - Percent bool `json:"percent"` - Format int `json:"format"` - Criteria string `json:"criteria"` + AboveAverage bool `json:"above_average,omitempty"` + Percent bool `json:"percent,omitempty"` + Format int `json:"format,omitempty"` + Criteria string `json:"criteria,omitempty"` Value string `json:"value,omitempty"` Minimum string `json:"minimum,omitempty"` Maximum string `json:"maximum,omitempty"` From bef49e40eec508a6413e80ee5df7df52f7827424 Mon Sep 17 00:00:00 2001 From: davidborry Date: Sat, 27 Aug 2022 09:16:41 -0700 Subject: [PATCH 095/213] This closes #1330 update non existing sheet error messages (#1331) --- adjust_test.go | 2 +- calc_test.go | 2 +- cell_test.go | 6 +++--- chart_test.go | 4 ++-- col_test.go | 24 ++++++++++++------------ comment_test.go | 4 ++-- datavalidation_test.go | 6 +++--- errors.go | 4 ++-- excelize_test.go | 36 ++++++++++++++++++------------------ merge_test.go | 6 +++--- picture_test.go | 8 ++++---- pivotTable.go | 2 +- pivotTable_test.go | 8 ++++---- rows.go | 4 ++-- rows_test.go | 20 ++++++++++---------- shape_test.go | 2 +- sheet_test.go | 16 ++++++++-------- sheetpr_test.go | 12 ++++++------ sparkline_test.go | 2 +- stream_test.go | 2 +- styles.go | 8 ++++---- styles_test.go | 6 +++--- table_test.go | 4 ++-- 23 files changed, 94 insertions(+), 94 deletions(-) diff --git a/adjust_test.go b/adjust_test.go index 1d807054e4..c3501018dd 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -339,7 +339,7 @@ func TestAdjustHelper(t *testing.T) { assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) // Test adjustHelper on not exists worksheet. - assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN is not exist") + assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN does not exist") } func TestAdjustCalcChain(t *testing.T) { diff --git a/calc_test.go b/calc_test.go index 5822135f18..ef196dca8a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4330,7 +4330,7 @@ func TestCalcCellValue(t *testing.T) { // Test get calculated cell value on not exists worksheet. f = prepareCalcData(cellData) _, err = f.CalcCellValue("SheetN", "A1") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") // Test get calculated cell value with not support formula. f = prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=UNSUPPORT(A1)")) diff --git a/cell_test.go b/cell_test.go index 2a09a9dc9e..45970fcb89 100644 --- a/cell_test.go +++ b/cell_test.go @@ -373,7 +373,7 @@ func TestGetCellFormula(t *testing.T) { // Test get cell formula on not exist worksheet. f := NewFile() _, err := f.GetCellFormula("SheetN", "A1") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") // Test get cell formula on no formula cell. assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) @@ -555,7 +555,7 @@ func TestGetCellRichText(t *testing.T) { assert.EqualError(t, err, "strconv.Atoi: parsing \"x\": invalid syntax") // Test set cell rich text on not exists worksheet _, err = f.GetCellRichText("SheetN", "A1") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") // Test set cell rich text with illegal cell coordinates _, err = f.GetCellRichText("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) @@ -652,7 +652,7 @@ func TestSetCellRichText(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellRichText.xlsx"))) // Test set cell rich text on not exists worksheet - assert.EqualError(t, f.SetCellRichText("SheetN", "A1", richTextRun), "sheet SheetN is not exist") + assert.EqualError(t, f.SetCellRichText("SheetN", "A1", richTextRun), "sheet SheetN does not exist") // Test set cell rich text with illegal cell coordinates assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) richTextRun = []RichTextRun{{Text: strings.Repeat("s", TotalCellChars+1)}} diff --git a/chart_test.go b/chart_test.go index 9f2287e802..82c6903281 100644 --- a/chart_test.go +++ b/chart_test.go @@ -114,7 +114,7 @@ func TestAddChart(t *testing.T) { assert.EqualError(t, f.AddChart("Sheet1", "P1", ""), "unexpected end of JSON input") // Test add chart on not exists worksheet. - assert.EqualError(t, f.AddChart("SheetN", "P1", "{}"), "sheet SheetN is not exist") + assert.EqualError(t, f.AddChart("SheetN", "P1", "{}"), "sheet SheetN does not exist") assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"none":true,"show_legend_key":true},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet1", "X1", `{"type":"colStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) @@ -250,7 +250,7 @@ func TestDeleteChart(t *testing.T) { assert.NoError(t, f.DeleteChart("Sheet1", "P1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteChart.xlsx"))) // Test delete chart on not exists worksheet. - assert.EqualError(t, f.DeleteChart("SheetN", "A1"), "sheet SheetN is not exist") + assert.EqualError(t, f.DeleteChart("SheetN", "A1"), "sheet SheetN does not exist") // Test delete chart with invalid coordinates. assert.EqualError(t, f.DeleteChart("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) // Test delete chart on no chart worksheet. diff --git a/col_test.go b/col_test.go index df74523a27..eb97c12ae0 100644 --- a/col_test.go +++ b/col_test.go @@ -94,7 +94,7 @@ func TestColsError(t *testing.T) { t.FailNow() } _, err = f.Cols("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.Close()) } @@ -104,7 +104,7 @@ func TestGetColsError(t *testing.T) { t.FailNow() } _, err = f.GetCols("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.Close()) f = NewFile() @@ -205,7 +205,7 @@ func TestColumnVisibility(t *testing.T) { // Test get column visible on an inexistent worksheet. _, err = f.GetColVisible("SheetN", "F") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") // Test get column visible with illegal cell coordinates. _, err = f.GetColVisible("Sheet1", "*") @@ -215,7 +215,7 @@ func TestColumnVisibility(t *testing.T) { f.NewSheet("Sheet3") assert.NoError(t, f.SetColVisible("Sheet3", "E", false)) assert.EqualError(t, f.SetColVisible("Sheet1", "A:-1", true), newInvalidColumnNameError("-1").Error()) - assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist") + assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN does not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) }) @@ -243,7 +243,7 @@ func TestOutlineLevel(t *testing.T) { level, err = f.GetColOutlineLevel("SheetN", "A") assert.Equal(t, uint8(0), level) - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.SetColWidth("Sheet2", "A", "D", 13)) assert.EqualError(t, f.SetColWidth("Sheet2", "A", "D", MaxColumnWidth+1), ErrColumnWidth.Error()) @@ -253,10 +253,10 @@ func TestOutlineLevel(t *testing.T) { assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "D", 8), ErrOutlineLevel.Error()) assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 2, 8), ErrOutlineLevel.Error()) // Test set row outline level on not exists worksheet. - assert.EqualError(t, f.SetRowOutlineLevel("SheetN", 1, 4), "sheet SheetN is not exist") + assert.EqualError(t, f.SetRowOutlineLevel("SheetN", 1, 4), "sheet SheetN does not exist") // Test get row outline level on not exists worksheet. _, err = f.GetRowOutlineLevel("SheetN", 1) - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") // Test set and get column outline level with illegal cell coordinates. assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), newInvalidColumnNameError("*").Error()) @@ -264,7 +264,7 @@ func TestOutlineLevel(t *testing.T) { assert.EqualError(t, err, newInvalidColumnNameError("*").Error()) // Test set column outline level on not exists worksheet. - assert.EqualError(t, f.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN is not exist") + assert.EqualError(t, f.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN does not exist") assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), newInvalidRowNumberError(0).Error()) level, err = f.GetRowOutlineLevel("Sheet1", 2) @@ -292,7 +292,7 @@ func TestSetColStyle(t *testing.T) { styleID, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#94d3a2"],"pattern":1}}`) assert.NoError(t, err) // Test set column style on not exists worksheet. - assert.EqualError(t, f.SetColStyle("SheetN", "E", styleID), "sheet SheetN is not exist") + assert.EqualError(t, f.SetColStyle("SheetN", "E", styleID), "sheet SheetN does not exist") // Test set column style with illegal cell coordinates. assert.EqualError(t, f.SetColStyle("Sheet1", "*", styleID), newInvalidColumnNameError("*").Error()) assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", styleID), newInvalidColumnNameError("*").Error()) @@ -329,11 +329,11 @@ func TestColWidth(t *testing.T) { assert.EqualError(t, f.SetColWidth("Sheet1", "A", "*", 1), newInvalidColumnNameError("*").Error()) // Test set column width on not exists worksheet. - assert.EqualError(t, f.SetColWidth("SheetN", "B", "A", 12), "sheet SheetN is not exist") + assert.EqualError(t, f.SetColWidth("SheetN", "B", "A", 12), "sheet SheetN does not exist") // Test get column width on not exists worksheet. _, err = f.GetColWidth("SheetN", "A") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColWidth.xlsx"))) convertRowHeightToPixels(0) @@ -376,7 +376,7 @@ func TestRemoveCol(t *testing.T) { assert.EqualError(t, f.RemoveCol("Sheet1", "*"), newInvalidColumnNameError("*").Error()) // Test remove column on not exists worksheet. - assert.EqualError(t, f.RemoveCol("SheetN", "B"), "sheet SheetN is not exist") + assert.EqualError(t, f.RemoveCol("SheetN", "B"), "sheet SheetN does not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } diff --git a/comment_test.go b/comment_test.go index c2d9fe2eda..64e9968e76 100644 --- a/comment_test.go +++ b/comment_test.go @@ -31,7 +31,7 @@ func TestAddComments(t *testing.T) { assert.NoError(t, f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`)) // Test add comment on not exists worksheet. - assert.EqualError(t, f.AddComment("SheetN", "B7", `{"author":"Excelize: ","text":"This is a comment."}`), "sheet SheetN is not exist") + assert.EqualError(t, f.AddComment("SheetN", "B7", `{"author":"Excelize: ","text":"This is a comment."}`), "sheet SheetN does not exist") // Test add comment on with illegal cell coordinates assert.EqualError(t, f.AddComment("Sheet1", "A", `{"author":"Excelize: ","text":"This is a comment."}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { @@ -66,7 +66,7 @@ func TestDeleteComment(t *testing.T) { assert.NoError(t, f.DeleteComment("Sheet2", "C41")) assert.EqualValues(t, 0, len(f.GetComments()["Sheet2"])) // Test delete comment on not exists worksheet - assert.EqualError(t, f.DeleteComment("SheetN", "A1"), "sheet SheetN is not exist") + assert.EqualError(t, f.DeleteComment("SheetN", "A1"), "sheet SheetN does not exist") // Test delete comment with worksheet part f.Pkg.Delete("xl/worksheets/sheet1.xml") assert.NoError(t, f.DeleteComment("Sheet1", "A22")) diff --git a/datavalidation_test.go b/datavalidation_test.go index 88625d10a7..c0d91177eb 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -90,7 +90,7 @@ func TestDataValidation(t *testing.T) { // Test get data validation on no exists worksheet _, err = f.GetDataValidations("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.SaveAs(resultFile)) @@ -158,7 +158,7 @@ func TestDataValidationError(t *testing.T) { // Test add data validation on no exists worksheet. f = NewFile() - assert.EqualError(t, f.AddDataValidation("SheetN", nil), "sheet SheetN is not exist") + assert.EqualError(t, f.AddDataValidation("SheetN", nil), "sheet SheetN does not exist") } func TestDeleteDataValidation(t *testing.T) { @@ -201,7 +201,7 @@ func TestDeleteDataValidation(t *testing.T) { assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1:B2"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // Test delete data validation on no exists worksheet. - assert.EqualError(t, f.DeleteDataValidation("SheetN", "A1:B2"), "sheet SheetN is not exist") + assert.EqualError(t, f.DeleteDataValidation("SheetN", "A1:B2"), "sheet SheetN does not exist") // Test delete all data validations in the worksheet. assert.NoError(t, f.DeleteDataValidation("Sheet1")) diff --git a/errors.go b/errors.go index fbcef043a6..a31330cfe1 100644 --- a/errors.go +++ b/errors.go @@ -70,10 +70,10 @@ func newCellNameToCoordinatesError(cell string, err error) error { return fmt.Errorf("cannot convert cell %q to coordinates: %v", cell, err) } -// newNoExistSheetError defined the error message on receiving the not exist +// newNoExistSheetError defined the error message on receiving the non existing // sheet name. func newNoExistSheetError(name string) error { - return fmt.Errorf("sheet %s is not exist", name) + return fmt.Errorf("sheet %s does not exist", name) } // newNotWorksheetError defined the error message on receiving a sheet which diff --git a/excelize_test.go b/excelize_test.go index eac218f2c2..1460d4a6ac 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -29,7 +29,7 @@ func TestOpenFile(t *testing.T) { // Test get all the rows in a not exists worksheet. _, err = f.GetRows("Sheet4") - assert.EqualError(t, err, "sheet Sheet4 is not exist") + assert.EqualError(t, err, "sheet Sheet4 does not exist") // Test get all the rows in a worksheet. rows, err := f.GetRows("Sheet2") assert.NoError(t, err) @@ -59,9 +59,9 @@ func TestOpenFile(t *testing.T) { f.NewSheet(":\\/?*[]Maximum 31 characters allowed in sheet title.") // Test set worksheet name with illegal name. f.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title.") - assert.EqualError(t, f.SetCellInt("Sheet3", "A23", 10), "sheet Sheet3 is not exist") - assert.EqualError(t, f.SetCellStr("Sheet3", "b230", "10"), "sheet Sheet3 is not exist") - assert.EqualError(t, f.SetCellStr("Sheet10", "b230", "10"), "sheet Sheet10 is not exist") + assert.EqualError(t, f.SetCellInt("Sheet3", "A23", 10), "sheet Sheet3 does not exist") + assert.EqualError(t, f.SetCellStr("Sheet3", "b230", "10"), "sheet Sheet3 does not exist") + assert.EqualError(t, f.SetCellStr("Sheet10", "b230", "10"), "sheet Sheet10 does not exist") // Test set cell string value with illegal row number. assert.EqualError(t, f.SetCellStr("Sheet1", "A", "10"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) @@ -122,11 +122,11 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet2", "F17", complex64(5+10i))) // Test on not exists worksheet. - assert.EqualError(t, f.SetCellDefault("SheetN", "A1", ""), "sheet SheetN is not exist") - assert.EqualError(t, f.SetCellFloat("SheetN", "A1", 42.65418, 2, 32), "sheet SheetN is not exist") - assert.EqualError(t, f.SetCellBool("SheetN", "A1", true), "sheet SheetN is not exist") - assert.EqualError(t, f.SetCellFormula("SheetN", "A1", ""), "sheet SheetN is not exist") - assert.EqualError(t, f.SetCellHyperLink("SheetN", "A1", "Sheet1!A40", "Location"), "sheet SheetN is not exist") + assert.EqualError(t, f.SetCellDefault("SheetN", "A1", ""), "sheet SheetN does not exist") + assert.EqualError(t, f.SetCellFloat("SheetN", "A1", 42.65418, 2, 32), "sheet SheetN does not exist") + assert.EqualError(t, f.SetCellBool("SheetN", "A1", true), "sheet SheetN does not exist") + assert.EqualError(t, f.SetCellFormula("SheetN", "A1", ""), "sheet SheetN does not exist") + assert.EqualError(t, f.SetCellHyperLink("SheetN", "A1", "Sheet1!A40", "Location"), "sheet SheetN does not exist") // Test boolean write booltest := []struct { @@ -151,7 +151,7 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet2", "G4", time.Now())) assert.NoError(t, f.SetCellValue("Sheet2", "G4", time.Now().UTC())) - assert.EqualError(t, f.SetCellValue("SheetN", "A1", time.Now()), "sheet SheetN is not exist") + assert.EqualError(t, f.SetCellValue("SheetN", "A1", time.Now()), "sheet SheetN does not exist") // 02:46:40 assert.NoError(t, f.SetCellValue("Sheet2", "G5", time.Duration(1e13))) // Test completion column. @@ -401,7 +401,7 @@ func TestGetCellHyperLink(t *testing.T) { assert.NoError(t, err) t.Log(link, target) link, target, err = f.GetCellHyperLink("Sheet3", "H3") - assert.EqualError(t, err, "sheet Sheet3 is not exist") + assert.EqualError(t, err, "sheet Sheet3 does not exist") t.Log(link, target) assert.NoError(t, f.Close()) @@ -968,7 +968,7 @@ func TestCopySheetError(t *testing.T) { t.FailNow() } - assert.EqualError(t, f.copySheet(-1, -2), "sheet is not exist") + assert.EqualError(t, f.copySheet(-1, -2), "sheet does not exist") if !assert.EqualError(t, f.CopySheet(-1, -2), "invalid worksheet index") { t.FailNow() } @@ -984,7 +984,7 @@ func TestGetSheetComments(t *testing.T) { func TestSetSheetVisible(t *testing.T) { f := NewFile() f.WorkBook.Sheets.Sheet[0].Name = "SheetN" - assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "sheet SheetN is not exist") + assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "sheet SheetN does not exist") } func TestGetActiveSheetIndex(t *testing.T) { @@ -1002,7 +1002,7 @@ func TestRelsWriter(t *testing.T) { func TestGetSheetView(t *testing.T) { f := NewFile() _, err := f.getSheetView("SheetN", 0) - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") } func TestConditionalFormat(t *testing.T) { @@ -1067,7 +1067,7 @@ func TestConditionalFormat(t *testing.T) { // Test set invalid format set in conditional format. assert.EqualError(t, f.SetConditionalFormat(sheet1, "L1:L10", ""), "unexpected end of JSON input") // Set conditional format on not exists worksheet. - assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", "[]"), "sheet SheetN is not exist") + assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", "[]"), "sheet SheetN does not exist") err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) if !assert.NoError(t, err) { @@ -1230,7 +1230,7 @@ func TestProtectSheet(t *testing.T) { Password: "password", }), ErrUnsupportedHashAlgorithm.Error()) // Test protect not exists worksheet. - assert.EqualError(t, f.ProtectSheet("SheetN", nil), "sheet SheetN is not exist") + assert.EqualError(t, f.ProtectSheet("SheetN", nil), "sheet SheetN does not exist") } func TestUnprotectSheet(t *testing.T) { @@ -1239,7 +1239,7 @@ func TestUnprotectSheet(t *testing.T) { t.FailNow() } // Test remove protection on not exists worksheet. - assert.EqualError(t, f.UnprotectSheet("SheetN"), "sheet SheetN is not exist") + assert.EqualError(t, f.UnprotectSheet("SheetN"), "sheet SheetN does not exist") assert.NoError(t, f.UnprotectSheet("Sheet1")) assert.EqualError(t, f.UnprotectSheet("Sheet1", "password"), ErrUnprotectSheet.Error()) @@ -1267,7 +1267,7 @@ func TestUnprotectSheet(t *testing.T) { func TestSetDefaultTimeStyle(t *testing.T) { f := NewFile() // Test set default time style on not exists worksheet. - assert.EqualError(t, f.setDefaultTimeStyle("SheetN", "", 0), "sheet SheetN is not exist") + assert.EqualError(t, f.setDefaultTimeStyle("SheetN", "", 0), "sheet SheetN does not exist") // Test set default time style on invalid cell assert.EqualError(t, f.setDefaultTimeStyle("Sheet1", "", 42), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) diff --git a/merge_test.go b/merge_test.go index 597d4b58a8..f4d5f7ed8a 100644 --- a/merge_test.go +++ b/merge_test.go @@ -65,7 +65,7 @@ func TestMergeCell(t *testing.T) { assert.NoError(t, f.MergeCell("Sheet3", "N10", "O11")) // Test get merged cells on not exists worksheet. - assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN is not exist") + assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN does not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) assert.NoError(t, f.Close()) @@ -140,7 +140,7 @@ func TestGetMergeCells(t *testing.T) { // Test get merged cells on not exists worksheet. _, err = f.GetMergeCells("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.Close()) } @@ -170,7 +170,7 @@ func TestUnmergeCell(t *testing.T) { f = NewFile() assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) // Test unmerged area on not exists worksheet. - assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN is not exist") + assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN does not exist") ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) diff --git a/picture_test.go b/picture_test.go index 37ccdc560b..3ab1cc0d18 100644 --- a/picture_test.go +++ b/picture_test.go @@ -128,7 +128,7 @@ func TestGetPicture(t *testing.T) { // Try to get picture from a worksheet that doesn't contain any images. file, raw, err = f.GetPicture("Sheet3", "I9") - assert.EqualError(t, err, "sheet Sheet3 is not exist") + assert.EqualError(t, err, "sheet Sheet3 does not exist") assert.Empty(t, file) assert.Empty(t, raw) @@ -196,7 +196,7 @@ func TestAddPictureFromBytes(t *testing.T) { return true }) assert.Equal(t, 1, imageCount, "Duplicate image should only be stored once.") - assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), "", "logo", ".png", imgFile), "sheet SheetN is not exist") + assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), "", "logo", ".png", imgFile), "sheet SheetN does not exist") } func TestDeletePicture(t *testing.T) { @@ -207,7 +207,7 @@ func TestDeletePicture(t *testing.T) { assert.NoError(t, f.DeletePicture("Sheet1", "P1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture.xlsx"))) // Test delete picture on not exists worksheet. - assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN is not exist") + assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN does not exist") // Test delete picture with invalid coordinates. assert.EqualError(t, f.DeletePicture("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) assert.NoError(t, f.Close()) @@ -219,7 +219,7 @@ func TestDrawingResize(t *testing.T) { f := NewFile() // Test calculate drawing resize on not exists worksheet. _, _, _, _, err := f.drawingResize("SheetN", "A1", 1, 1, nil) - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") // Test calculate drawing resize with invalid coordinates. _, _, _, _, err = f.drawingResize("Sheet1", "", 1, 1, nil) assert.EqualError(t, err, newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) diff --git a/pivotTable.go b/pivotTable.go index bd9fee62ce..1ef0333501 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -190,7 +190,7 @@ func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet, } pivotTableSheetPath, ok := f.getSheetXMLPath(pivotTableSheetName) if !ok { - return dataSheet, pivotTableSheetPath, fmt.Errorf("sheet %s is not exist", pivotTableSheetName) + return dataSheet, pivotTableSheetPath, fmt.Errorf("sheet %s does not exist", pivotTableSheetName) } return dataSheet, pivotTableSheetPath, err } diff --git a/pivotTable_test.go b/pivotTable_test.go index adba2eb197..ed79298322 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -182,7 +182,7 @@ func TestAddPivotTable(t *testing.T) { Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, - }), "sheet SheetN is not exist") + }), "sheet SheetN does not exist") // Test the pivot table range of the worksheet that is not declared assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", @@ -198,7 +198,7 @@ func TestAddPivotTable(t *testing.T) { Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, - }), "sheet SheetN is not exist") + }), "sheet SheetN does not exist") // Test not exists worksheet in data range assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "SheetN!$A$1:$E$31", @@ -206,7 +206,7 @@ func TestAddPivotTable(t *testing.T) { Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, - }), "sheet SheetN is not exist") + }), "sheet SheetN does not exist") // Test invalid row number in data range assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$0:$E$31", @@ -298,7 +298,7 @@ func TestGetPivotFieldsOrder(t *testing.T) { f := NewFile() // Test get pivot fields order with not exist worksheet _, err := f.getPivotFieldsOrder(&PivotTableOption{DataRange: "SheetN!$A$1:$E$31"}) - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") } func TestGetPivotTableFieldName(t *testing.T) { diff --git a/rows.go b/rows.go index 58085302a3..7e2d6774df 100644 --- a/rows.go +++ b/rows.go @@ -202,13 +202,13 @@ func appendSpace(l int, s []string) []string { return s } -// ErrSheetNotExist defines an error of sheet is not exist +// ErrSheetNotExist defines an error of sheet that does not exist type ErrSheetNotExist struct { SheetName string } func (err ErrSheetNotExist) Error() string { - return fmt.Sprintf("sheet %s is not exist", err.SheetName) + return fmt.Sprintf("sheet %s does not exist", err.SheetName) } // rowXMLIterator defined runtime use field for the worksheet row SAX parser. diff --git a/rows_test.go b/rows_test.go index 585401b52f..cac142b13a 100644 --- a/rows_test.go +++ b/rows_test.go @@ -128,7 +128,7 @@ func TestRowsError(t *testing.T) { t.FailNow() } _, err = f.Rows("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.Close()) } @@ -160,9 +160,9 @@ func TestRowHeight(t *testing.T) { assert.Equal(t, defaultRowHeight, height) // Test set and get row height on not exists worksheet. - assert.EqualError(t, f.SetRowHeight("SheetN", 1, 111.0), "sheet SheetN is not exist") + assert.EqualError(t, f.SetRowHeight("SheetN", 1, 111.0), "sheet SheetN does not exist") _, err = f.GetRowHeight("SheetN", 3) - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") // Test get row height with custom default row height. assert.NoError(t, f.SetSheetFormatPr(sheet1, @@ -246,13 +246,13 @@ func TestRowVisibility(t *testing.T) { assert.Equal(t, false, visible) assert.NoError(t, err) assert.EqualError(t, f.SetRowVisible("Sheet3", 0, true), newInvalidRowNumberError(0).Error()) - assert.EqualError(t, f.SetRowVisible("SheetN", 2, false), "sheet SheetN is not exist") + assert.EqualError(t, f.SetRowVisible("SheetN", 2, false), "sheet SheetN does not exist") visible, err = f.GetRowVisible("Sheet3", 0) assert.Equal(t, false, visible) assert.EqualError(t, err, newInvalidRowNumberError(0).Error()) _, err = f.GetRowVisible("SheetN", 1) - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) } @@ -315,7 +315,7 @@ func TestRemoveRow(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) // Test remove row on not exist worksheet - assert.EqualError(t, f.RemoveRow("SheetN", 1), `sheet SheetN is not exist`) + assert.EqualError(t, f.RemoveRow("SheetN", 1), `sheet SheetN does not exist`) } func TestInsertRow(t *testing.T) { @@ -878,7 +878,7 @@ func TestDuplicateRowTo(t *testing.T) { }) assert.EqualError(t, f.DuplicateRowTo(sheetName, 1, 2), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // Test duplicate row on not exists worksheet - assert.EqualError(t, f.DuplicateRowTo("SheetN", 1, 2), "sheet SheetN is not exist") + assert.EqualError(t, f.DuplicateRowTo("SheetN", 1, 2), "sheet SheetN does not exist") } func TestDuplicateMergeCells(t *testing.T) { @@ -888,7 +888,7 @@ func TestDuplicateMergeCells(t *testing.T) { }} assert.EqualError(t, f.duplicateMergeCells("Sheet1", ws, 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`) ws.MergeCells.Cells[0].Ref = "A1:B1" - assert.EqualError(t, f.duplicateMergeCells("SheetN", ws, 1, 2), "sheet SheetN is not exist") + assert.EqualError(t, f.duplicateMergeCells("SheetN", ws, 1, 2), "sheet SheetN does not exist") } func TestGetValueFromInlineStr(t *testing.T) { @@ -923,7 +923,7 @@ func TestGetValueFromNumber(t *testing.T) { func TestErrSheetNotExistError(t *testing.T) { err := ErrSheetNotExist{SheetName: "Sheet1"} - assert.EqualValues(t, err.Error(), "sheet Sheet1 is not exist") + assert.EqualValues(t, err.Error(), "sheet Sheet1 does not exist") } func TestCheckRow(t *testing.T) { @@ -949,7 +949,7 @@ func TestSetRowStyle(t *testing.T) { assert.EqualError(t, f.SetRowStyle("Sheet1", 5, -1, style2), newInvalidRowNumberError(-1).Error()) assert.EqualError(t, f.SetRowStyle("Sheet1", 1, TotalRows+1, style2), ErrMaxRows.Error()) assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, -1), newInvalidStyleID(-1).Error()) - assert.EqualError(t, f.SetRowStyle("SheetN", 1, 1, style2), "sheet SheetN is not exist") + assert.EqualError(t, f.SetRowStyle("SheetN", 1, 1, style2), "sheet SheetN does not exist") assert.NoError(t, f.SetRowStyle("Sheet1", 5, 1, style2)) cellStyleID, err := f.GetCellStyle("Sheet1", "B2") assert.NoError(t, err) diff --git a/shape_test.go b/shape_test.go index 2f005894d0..829a9e5e46 100644 --- a/shape_test.go +++ b/shape_test.go @@ -36,7 +36,7 @@ func TestAddShape(t *testing.T) { } }], "height": 90 - }`), "sheet Sheet3 is not exist") + }`), "sheet Sheet3 does not exist") assert.EqualError(t, f.AddShape("Sheet3", "H1", ""), "unexpected end of JSON input") assert.EqualError(t, f.AddShape("Sheet1", "A", `{ "type": "rect", diff --git a/sheet_test.go b/sheet_test.go index 9b0caf4894..4df62bff65 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -102,7 +102,7 @@ func TestSetPane(t *testing.T) { f.NewSheet("Panes 4") assert.NoError(t, f.SetPanes("Panes 4", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`)) assert.NoError(t, f.SetPanes("Panes 4", "")) - assert.EqualError(t, f.SetPanes("SheetN", ""), "sheet SheetN is not exist") + assert.EqualError(t, f.SetPanes("SheetN", ""), "sheet SheetN does not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) // Test add pane on empty sheet views worksheet f = NewFile() @@ -181,7 +181,7 @@ func TestSearchSheet(t *testing.T) { } // Test search in a not exists worksheet. _, err = f.SearchSheet("Sheet4", "") - assert.EqualError(t, err, "sheet Sheet4 is not exist") + assert.EqualError(t, err, "sheet Sheet4 does not exist") var expected []string // Test search a not exists value. result, err := f.SearchSheet("Sheet1", "X") @@ -225,20 +225,20 @@ func TestSearchSheet(t *testing.T) { func TestSetPageLayout(t *testing.T) { f := NewFile() // Test set page layout on not exists worksheet. - assert.EqualError(t, f.SetPageLayout("SheetN"), "sheet SheetN is not exist") + assert.EqualError(t, f.SetPageLayout("SheetN"), "sheet SheetN does not exist") } func TestGetPageLayout(t *testing.T) { f := NewFile() // Test get page layout on not exists worksheet. - assert.EqualError(t, f.GetPageLayout("SheetN"), "sheet SheetN is not exist") + assert.EqualError(t, f.GetPageLayout("SheetN"), "sheet SheetN does not exist") } func TestSetHeaderFooter(t *testing.T) { f := NewFile() assert.NoError(t, f.SetCellStr("Sheet1", "A1", "Test SetHeaderFooter")) // Test set header and footer on not exists worksheet. - assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN is not exist") + assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN does not exist") // Test set header and footer with illegal setting. assert.EqualError(t, f.SetHeaderFooter("Sheet1", &FormatHeaderFooter{ OddHeader: strings.Repeat("c", MaxFieldLength+1), @@ -301,7 +301,7 @@ func TestGroupSheets(t *testing.T) { for _, sheet := range sheets { f.NewSheet(sheet) } - assert.EqualError(t, f.GroupSheets([]string{"Sheet1", "SheetN"}), "sheet SheetN is not exist") + assert.EqualError(t, f.GroupSheets([]string{"Sheet1", "SheetN"}), "sheet SheetN does not exist") assert.EqualError(t, f.GroupSheets([]string{"Sheet2", "Sheet3"}), "group worksheet must contain an active worksheet") assert.NoError(t, f.GroupSheets([]string{"Sheet1", "Sheet2"})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGroupSheets.xlsx"))) @@ -323,7 +323,7 @@ func TestInsertPageBreak(t *testing.T) { assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) assert.EqualError(t, f.InsertPageBreak("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, f.InsertPageBreak("SheetN", "C3"), "sheet SheetN is not exist") + assert.EqualError(t, f.InsertPageBreak("SheetN", "C3"), "sheet SheetN does not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertPageBreak.xlsx"))) } @@ -349,7 +349,7 @@ func TestRemovePageBreak(t *testing.T) { assert.NoError(t, f.RemovePageBreak("Sheet2", "B2")) assert.EqualError(t, f.RemovePageBreak("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, f.RemovePageBreak("SheetN", "C3"), "sheet SheetN is not exist") + assert.EqualError(t, f.RemovePageBreak("SheetN", "C3"), "sheet SheetN does not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemovePageBreak.xlsx"))) } diff --git a/sheetpr_test.go b/sheetpr_test.go index 65ab196f38..047e5f1d3c 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -180,13 +180,13 @@ func TestSetSheetPrOptions(t *testing.T) { f := NewFile() assert.NoError(t, f.SetSheetPrOptions("Sheet1", TabColorRGB(""))) // Test SetSheetPrOptions on not exists worksheet. - assert.EqualError(t, f.SetSheetPrOptions("SheetN"), "sheet SheetN is not exist") + assert.EqualError(t, f.SetSheetPrOptions("SheetN"), "sheet SheetN does not exist") } func TestGetSheetPrOptions(t *testing.T) { f := NewFile() // Test GetSheetPrOptions on not exists worksheet. - assert.EqualError(t, f.GetSheetPrOptions("SheetN"), "sheet SheetN is not exist") + assert.EqualError(t, f.GetSheetPrOptions("SheetN"), "sheet SheetN does not exist") } var _ = []PageMarginsOptions{ @@ -328,13 +328,13 @@ func TestPageMarginsOption(t *testing.T) { func TestSetPageMargins(t *testing.T) { f := NewFile() // Test set page margins on not exists worksheet. - assert.EqualError(t, f.SetPageMargins("SheetN"), "sheet SheetN is not exist") + assert.EqualError(t, f.SetPageMargins("SheetN"), "sheet SheetN does not exist") } func TestGetPageMargins(t *testing.T) { f := NewFile() // Test get page margins on not exists worksheet. - assert.EqualError(t, f.GetPageMargins("SheetN"), "sheet SheetN is not exist") + assert.EqualError(t, f.GetPageMargins("SheetN"), "sheet SheetN does not exist") } func ExampleFile_SetSheetFormatPr() { @@ -469,7 +469,7 @@ func TestSetSheetFormatPr(t *testing.T) { ws.(*xlsxWorksheet).SheetFormatPr = nil assert.NoError(t, f.SetSheetFormatPr("Sheet1", BaseColWidth(1.0))) // Test set formatting properties on not exists worksheet. - assert.EqualError(t, f.SetSheetFormatPr("SheetN"), "sheet SheetN is not exist") + assert.EqualError(t, f.SetSheetFormatPr("SheetN"), "sheet SheetN does not exist") } func TestGetSheetFormatPr(t *testing.T) { @@ -497,5 +497,5 @@ func TestGetSheetFormatPr(t *testing.T) { &thickBottom, )) // Test get formatting properties on not exists worksheet. - assert.EqualError(t, f.GetSheetFormatPr("SheetN"), "sheet SheetN is not exist") + assert.EqualError(t, f.GetSheetFormatPr("SheetN"), "sheet SheetN does not exist") } diff --git a/sparkline_test.go b/sparkline_test.go index c21687cbb9..4703c859d6 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -218,7 +218,7 @@ func TestAddSparkline(t *testing.T) { assert.EqualError(t, f.AddSparkline("SheetN", &SparklineOption{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, - }), "sheet SheetN is not exist") + }), "sheet SheetN does not exist") assert.EqualError(t, f.AddSparkline("Sheet1", nil), ErrParameterRequired.Error()) diff --git a/stream_test.go b/stream_test.go index 8f6a5b4cf5..029082ca34 100644 --- a/stream_test.go +++ b/stream_test.go @@ -198,7 +198,7 @@ func TestNewStreamWriter(t *testing.T) { _, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) _, err = file.NewStreamWriter("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") } func TestSetRow(t *testing.T) { diff --git a/styles.go b/styles.go index c770360492..55ee17552d 100644 --- a/styles.go +++ b/styles.go @@ -1966,7 +1966,7 @@ var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ } // getStyleID provides a function to get styleID by given style. If given -// style is not exist, will return -1. +// style does not exist, will return -1. func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (styleID int) { styleID = -1 if ss.CellXfs == nil { @@ -2047,7 +2047,7 @@ func (f *File) readDefaultFont() *xlsxFont { } // getFontID provides a function to get font ID. -// If given font is not exist, will return -1. +// If given font does not exist, will return -1. func (f *File) getFontID(styleSheet *xlsxStyleSheet, style *Style) (fontID int) { fontID = -1 if styleSheet.Fonts == nil || style.Font == nil { @@ -2098,7 +2098,7 @@ func (f *File) newFont(style *Style) *xlsxFont { } // getNumFmtID provides a function to get number format code ID. -// If given number format code is not exist, will return -1. +// If given number format code does not exist, will return -1. func getNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (numFmtID int) { numFmtID = -1 if _, ok := builtInNumFmt[style.NumFmt]; ok { @@ -2195,7 +2195,7 @@ func setCustomNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { } // getCustomNumFmtID provides a function to get custom number format code ID. -// If given custom number format code is not exist, will return -1. +// If given custom number format code does not exist, will return -1. func getCustomNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (customNumFmtID int) { customNumFmtID = -1 if styleSheet.NumFmts == nil { diff --git a/styles_test.go b/styles_test.go index 96cc2adb92..257f98d4f2 100644 --- a/styles_test.go +++ b/styles_test.go @@ -199,7 +199,7 @@ func TestGetConditionalFormats(t *testing.T) { // Test get conditional formats on no exists worksheet f := NewFile() _, err := f.GetConditionalFormats("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN does not exist") } func TestUnsetConditionalFormat(t *testing.T) { @@ -211,7 +211,7 @@ func TestUnsetConditionalFormat(t *testing.T) { assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format))) assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10")) // Test unset conditional format on not exists worksheet. - assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN is not exist") + assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN does not exist") // Save spreadsheet by the given path. assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx"))) } @@ -341,7 +341,7 @@ func TestThemeReader(t *testing.T) { func TestSetCellStyle(t *testing.T) { f := NewFile() // Test set cell style on not exists worksheet. - assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN is not exist") + assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN does not exist") } func TestGetStyleID(t *testing.T) { diff --git a/table_test.go b/table_test.go index 5941c508ab..39b418ba7c 100644 --- a/table_test.go +++ b/table_test.go @@ -30,7 +30,7 @@ func TestAddTable(t *testing.T) { } // Test add table in not exist worksheet. - assert.EqualError(t, f.AddTable("SheetN", "B26", "A21", `{}`), "sheet SheetN is not exist") + assert.EqualError(t, f.AddTable("SheetN", "B26", "A21", `{}`), "sheet SheetN does not exist") // Test add table with illegal formatset. assert.EqualError(t, f.AddTable("Sheet1", "B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") // Test add table with illegal cell coordinates. @@ -111,7 +111,7 @@ func TestAutoFilterError(t *testing.T) { assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, &formatAutoFilter{ Column: "A", Expression: "", - }), "sheet SheetN is not exist") + }), "sheet SheetN does not exist") assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &formatAutoFilter{ Column: "-", Expression: "-", From 18cd63a548afa1abcddc86a998fdefa3b4cc60c1 Mon Sep 17 00:00:00 2001 From: Kostya Privezentsev Date: Tue, 30 Aug 2022 19:02:48 +0300 Subject: [PATCH 096/213] This is a breaking change closes #1332 (#1333) This use `InsertRows` instead of `InsertRow`, and using `InsertCols` instead of `InsertCol` --- adjust.go | 19 +++++++++++++++---- adjust_test.go | 8 ++++---- col.go | 19 ++++++++++++++----- col_test.go | 12 ++++++++---- rows.go | 18 ++++++++++++------ rows_test.go | 32 ++++++++++++++++++++------------ 6 files changed, 73 insertions(+), 35 deletions(-) diff --git a/adjust.go b/adjust.go index 99d2850913..fd570bb6a2 100644 --- a/adjust.go +++ b/adjust.go @@ -42,9 +42,12 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) } sheetID := f.getSheetID(sheet) if dir == rows { - f.adjustRowDimensions(ws, num, offset) + err = f.adjustRowDimensions(ws, num, offset) } else { - f.adjustColDimensions(ws, num, offset) + err = f.adjustColDimensions(ws, num, offset) + } + if err != nil { + return err } f.adjustHyperlinks(ws, sheet, dir, num, offset) f.adjustTable(ws, sheet, dir, num, offset) @@ -69,28 +72,36 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) // adjustColDimensions provides a function to update column dimensions when // inserting or deleting rows or columns. -func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) { +func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) error { for rowIdx := range ws.SheetData.Row { for colIdx, v := range ws.SheetData.Row[rowIdx].C { cellCol, cellRow, _ := CellNameToCoordinates(v.R) if col <= cellCol { if newCol := cellCol + offset; newCol > 0 { + if newCol > MaxColumns { + return ErrColumnNumber + } ws.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow) } } } } + return nil } // adjustRowDimensions provides a function to update row dimensions when // inserting or deleting rows or columns. -func (f *File) adjustRowDimensions(ws *xlsxWorksheet, row, offset int) { +func (f *File) adjustRowDimensions(ws *xlsxWorksheet, row, offset int) error { for i := range ws.SheetData.Row { r := &ws.SheetData.Row[i] if newRow := r.R + offset; r.R >= row && newRow > 0 { + if newRow >= TotalRows { + return ErrMaxRows + } f.adjustSingleRowDimensions(r, newRow) } } + return nil } // adjustSingleRowDimensions provides a function to adjust single row dimensions. diff --git a/adjust_test.go b/adjust_test.go index c3501018dd..aa374da3bf 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -349,11 +349,11 @@ func TestAdjustCalcChain(t *testing.T) { {R: "B2", I: 2}, {R: "B2", I: 1}, }, } - assert.NoError(t, f.InsertCol("Sheet1", "A")) - assert.NoError(t, f.InsertRow("Sheet1", 1)) + assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) + assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) f.CalcChain.C[1].R = "invalid coordinates" - assert.EqualError(t, f.InsertCol("Sheet1", "A"), newCellNameToCoordinatesError("invalid coordinates", newInvalidCellNameError("invalid coordinates")).Error()) + assert.EqualError(t, f.InsertCols("Sheet1", "A", 1), newCellNameToCoordinatesError("invalid coordinates", newInvalidCellNameError("invalid coordinates")).Error()) f.CalcChain = nil - assert.NoError(t, f.InsertCol("Sheet1", "A")) + assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) } diff --git a/col.go b/col.go index 248e22c27b..f51336d3ad 100644 --- a/col.go +++ b/col.go @@ -657,16 +657,25 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { return defaultColWidth, err } -// InsertCol provides a function to insert a new column before given column -// index. For example, create a new column before column C in Sheet1: +// InsertCols provides a function to insert new columns before the given column +// name and number of columns. For example, create two columns before column +// C in Sheet1: // -// err := f.InsertCol("Sheet1", "C") -func (f *File) InsertCol(sheet, col string) error { +// err := f.InsertCols("Sheet1", "C", 2) +// +// Use this method with caution, which will affect changes in references such +// as formulas, charts, and so on. If there is any referenced value of the +// worksheet, it will cause a file error when you open it. The excelize only +// partially updates these references currently. +func (f *File) InsertCols(sheet, col string, n int) error { num, err := ColumnNameToNumber(col) if err != nil { return err } - return f.adjustHelper(sheet, columns, num, 1) + if n < 1 || n > MaxColumns { + return ErrColumnNumber + } + return f.adjustHelper(sheet, columns, num, n) } // RemoveCol provides a function to remove single column by given worksheet diff --git a/col_test.go b/col_test.go index eb97c12ae0..b7d382366f 100644 --- a/col_test.go +++ b/col_test.go @@ -339,7 +339,7 @@ func TestColWidth(t *testing.T) { convertRowHeightToPixels(0) } -func TestInsertCol(t *testing.T) { +func TestInsertCols(t *testing.T) { f := NewFile() sheet1 := f.GetSheetName(0) @@ -349,12 +349,16 @@ func TestInsertCol(t *testing.T) { assert.NoError(t, f.MergeCell(sheet1, "A1", "C3")) assert.NoError(t, f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`)) - assert.NoError(t, f.InsertCol(sheet1, "A")) + assert.NoError(t, f.InsertCols(sheet1, "A", 1)) // Test insert column with illegal cell coordinates. - assert.EqualError(t, f.InsertCol("Sheet1", "*"), newInvalidColumnNameError("*").Error()) + assert.EqualError(t, f.InsertCols(sheet1, "*", 1), newInvalidColumnNameError("*").Error()) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) + assert.EqualError(t, f.InsertCols(sheet1, "A", 0), ErrColumnNumber.Error()) + assert.EqualError(t, f.InsertCols(sheet1, "A", MaxColumns), ErrColumnNumber.Error()) + assert.EqualError(t, f.InsertCols(sheet1, "A", MaxColumns-10), ErrColumnNumber.Error()) + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertCols.xlsx"))) } func TestRemoveCol(t *testing.T) { diff --git a/rows.go b/rows.go index 7e2d6774df..9269ac6311 100644 --- a/rows.go +++ b/rows.go @@ -622,21 +622,27 @@ func (f *File) RemoveRow(sheet string, row int) error { return f.adjustHelper(sheet, rows, row, -1) } -// InsertRow provides a function to insert a new row after given Excel row -// number starting from 1. For example, create a new row before row 3 in -// Sheet1: +// InsertRows provides a function to insert new rows after the given Excel row +// number starting from 1 and number of rows. For example, create two rows +// before row 3 in Sheet1: // -// err := f.InsertRow("Sheet1", 3) +// err := f.InsertRows("Sheet1", 3, 2) // // Use this method with caution, which will affect changes in references such // as formulas, charts, and so on. If there is any referenced value of the // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. -func (f *File) InsertRow(sheet string, row int) error { +func (f *File) InsertRows(sheet string, row, n int) error { if row < 1 { return newInvalidRowNumberError(row) } - return f.adjustHelper(sheet, rows, row, 1) + if row >= TotalRows || n >= TotalRows { + return ErrMaxRows + } + if n < 1 { + return ErrParameterInvalid + } + return f.adjustHelper(sheet, rows, row, n) } // DuplicateRow inserts a copy of specified row (by its Excel row number) below diff --git a/rows_test.go b/rows_test.go index cac142b13a..829a27aef1 100644 --- a/rows_test.go +++ b/rows_test.go @@ -318,7 +318,7 @@ func TestRemoveRow(t *testing.T) { assert.EqualError(t, f.RemoveRow("SheetN", 1), `sheet SheetN does not exist`) } -func TestInsertRow(t *testing.T) { +func TestInsertRows(t *testing.T) { f := NewFile() sheet1 := f.GetSheetName(0) r, err := f.workSheetReader(sheet1) @@ -331,36 +331,44 @@ func TestInsertRow(t *testing.T) { assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) - assert.EqualError(t, f.InsertRow(sheet1, -1), newInvalidRowNumberError(-1).Error()) - - assert.EqualError(t, f.InsertRow(sheet1, 0), newInvalidRowNumberError(0).Error()) - - assert.NoError(t, f.InsertRow(sheet1, 1)) + assert.NoError(t, f.InsertRows(sheet1, 1, 1)) if !assert.Len(t, r.SheetData.Row, rowCount+1) { t.FailNow() } - assert.NoError(t, f.InsertRow(sheet1, 4)) + assert.NoError(t, f.InsertRows(sheet1, 4, 1)) if !assert.Len(t, r.SheetData.Row, rowCount+2) { t.FailNow() } - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRow.xlsx"))) + assert.NoError(t, f.InsertRows(sheet1, 4, 2)) + if !assert.Len(t, r.SheetData.Row, rowCount+4) { + t.FailNow() + } + + assert.EqualError(t, f.InsertRows(sheet1, -1, 1), newInvalidRowNumberError(-1).Error()) + assert.EqualError(t, f.InsertRows(sheet1, 0, 1), newInvalidRowNumberError(0).Error()) + assert.EqualError(t, f.InsertRows(sheet1, 4, 0), ErrParameterInvalid.Error()) + assert.EqualError(t, f.InsertRows(sheet1, 4, TotalRows), ErrMaxRows.Error()) + assert.EqualError(t, f.InsertRows(sheet1, 4, TotalRows-5), ErrMaxRows.Error()) + assert.EqualError(t, f.InsertRows(sheet1, TotalRows, 1), ErrMaxRows.Error()) + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRows.xlsx"))) } // Test internal structure state after insert operations. It is important // for insert workflow to be constant to avoid side effect with functions // related to internal structure. -func TestInsertRowInEmptyFile(t *testing.T) { +func TestInsertRowsInEmptyFile(t *testing.T) { f := NewFile() sheet1 := f.GetSheetName(0) r, err := f.workSheetReader(sheet1) assert.NoError(t, err) - assert.NoError(t, f.InsertRow(sheet1, 1)) + assert.NoError(t, f.InsertRows(sheet1, 1, 1)) assert.Len(t, r.SheetData.Row, 0) - assert.NoError(t, f.InsertRow(sheet1, 2)) + assert.NoError(t, f.InsertRows(sheet1, 2, 1)) assert.Len(t, r.SheetData.Row, 0) - assert.NoError(t, f.InsertRow(sheet1, 99)) + assert.NoError(t, f.InsertRows(sheet1, 99, 1)) assert.Len(t, r.SheetData.Row, 0) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) } From 75ce2317286181e2c250c10206df892278d5b981 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 1 Sep 2022 00:41:52 +0800 Subject: [PATCH 097/213] This closes #1323, an error will be returned when set the not exist style ID --- calc.go | 10 +++++----- col.go | 7 +++++++ col_test.go | 4 ++++ errors.go | 2 +- excelize_test.go | 45 +++++++++++++++++++++++++++++++++++---------- rows.go | 5 ++++- rows_test.go | 3 +++ styles.go | 8 ++++++++ styles_test.go | 4 ++++ 9 files changed, 71 insertions(+), 17 deletions(-) diff --git a/calc.go b/calc.go index 25595d6850..f6217a8778 100644 --- a/calc.go +++ b/calc.go @@ -6037,7 +6037,7 @@ func getBetaHelperContFrac(fX, fA, fB float64) float64 { bfinished = math.Abs(cf-cfnew) < math.Abs(cf)*fMachEps } cf = cfnew - rm += 1 + rm++ } return cf } @@ -6914,7 +6914,7 @@ func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { for z <= x1 { e = math.Log(z) + e s += math.Exp(c*z - a - e) - z += 1 + z++ } return newNumberFormulaArg(s) } @@ -6926,7 +6926,7 @@ func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { for z <= x1 { e = e * (a / z) c = c + e - z += 1 + z++ } return newNumberFormulaArg(c*y + s) } @@ -15286,10 +15286,10 @@ func (fn *formulaFuncs) coupons(name string, arg formulaArg) formulaArg { month -= coupon } if month > 11 { - year += 1 + year++ month -= 12 } else if month < 0 { - year -= 1 + year-- month += 12 } day, lastDay := maturity.Day(), time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) diff --git a/col.go b/col.go index f51336d3ad..c0deb58cc0 100644 --- a/col.go +++ b/col.go @@ -415,6 +415,13 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { if err != nil { return err } + s := f.stylesReader() + s.Lock() + if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { + s.Unlock() + return newInvalidStyleID(styleID) + } + s.Unlock() ws, err := f.workSheetReader(sheet) if err != nil { return err diff --git a/col_test.go b/col_test.go index b7d382366f..1076f31c72 100644 --- a/col_test.go +++ b/col_test.go @@ -296,6 +296,10 @@ func TestSetColStyle(t *testing.T) { // Test set column style with illegal cell coordinates. assert.EqualError(t, f.SetColStyle("Sheet1", "*", styleID), newInvalidColumnNameError("*").Error()) assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", styleID), newInvalidColumnNameError("*").Error()) + // Test set column style with invalid style ID. + assert.EqualError(t, f.SetColStyle("Sheet1", "B", -1), newInvalidStyleID(-1).Error()) + // Test set column style with not exists style ID. + assert.EqualError(t, f.SetColStyle("Sheet1", "B", 10), newInvalidStyleID(10).Error()) assert.NoError(t, f.SetColStyle("Sheet1", "B", styleID)) // Test set column style with already exists column with style. diff --git a/errors.go b/errors.go index a31330cfe1..48476bc406 100644 --- a/errors.go +++ b/errors.go @@ -55,7 +55,7 @@ func newUnzipSizeLimitError(unzipSizeLimit int64) error { // newInvalidStyleID defined the error message on receiving the invalid style // ID. func newInvalidStyleID(styleID int) error { - return fmt.Errorf("invalid style ID %d, negative values are not supported", styleID) + return fmt.Errorf("invalid style ID %d", styleID) } // newFieldLengthError defined the error message on receiving the field length diff --git a/excelize_test.go b/excelize_test.go index 1460d4a6ac..19aba7eeaf 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -32,13 +32,22 @@ func TestOpenFile(t *testing.T) { assert.EqualError(t, err, "sheet Sheet4 does not exist") // Test get all the rows in a worksheet. rows, err := f.GetRows("Sheet2") - assert.NoError(t, err) - for _, row := range rows { - for _, cell := range row { - t.Log(cell, "\t") - } - t.Log("\r\n") + expected := [][]string{ + {"Monitor", "", "Brand", "", "inlineStr"}, + {"> 23 Inch", "19", "HP", "200"}, + {"20-23 Inch", "24", "DELL", "450"}, + {"17-20 Inch", "56", "Lenove", "200"}, + {"< 17 Inch", "21", "SONY", "510"}, + {"", "", "Acer", "315"}, + {"", "", "IBM", "127"}, + {"", "", "ASUS", "89"}, + {"", "", "Apple", "348"}, + {"", "", "SAMSUNG", "53"}, + {"", "", "Other", "37", "", "", "", "", ""}, } + assert.NoError(t, err) + assert.Equal(t, expected, rows) + assert.NoError(t, f.UpdateLinkedValue()) assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(100.1588, 'f', -1, 32))) @@ -396,13 +405,19 @@ func TestGetCellHyperLink(t *testing.T) { link, target, err := f.GetCellHyperLink("Sheet1", "A22") assert.NoError(t, err) - t.Log(link, target) + assert.Equal(t, link, true) + assert.Equal(t, target, "https://github.com/xuri/excelize") + link, target, err = f.GetCellHyperLink("Sheet2", "D6") assert.NoError(t, err) - t.Log(link, target) + assert.Equal(t, link, false) + assert.Equal(t, target, "") + link, target, err = f.GetCellHyperLink("Sheet3", "H3") assert.EqualError(t, err, "sheet Sheet3 does not exist") - t.Log(link, target) + assert.Equal(t, link, false) + assert.Equal(t, target, "") + assert.NoError(t, f.Close()) f = NewFile() @@ -709,6 +724,14 @@ func TestSetCellStyleNumberFormat(t *testing.T) { col := []string{"L", "M", "N", "O", "P"} data := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49} value := []string{"37947.7500001", "-37947.7500001", "0.007", "2.1", "String"} + expected := [][]string{ + {"37947.7500001", "37948", "37947.75", "37948", "37947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 pm", "6:00:00 pm", "18:00", "18:00:00", "11/22/03 18:00", "37947", "37947", "37947.75", "37947.75", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "37947.7500001", "3.79E+04", "37947.7500001"}, + {"-37947.7500001", "-37948", "-37947.75", "-37948", "-37947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37947)", "(37947)", "(-37947.75)", "(-37947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-3.79E+04", "-37947.7500001"}, + {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 am", "0:10:04 am", "00:10", "00:10:04", "12/30/99 00:10", "0", "0", "0.01", "0.01", "0.007", "0.007", "0.007", "0.007", "10:04", "0:10:04", "0.007", "7.00E-03", "0.007"}, + {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 am", "2:24:00 am", "02:24", "02:24:00", "1/1/00 02:24", "2", "2", "2.10", "2.10", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "2.1", "2.10E+00", "2.1"}, + {"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String"}, + } + for i, v := range value { for k, d := range data { c := col[i] + strconv.Itoa(k+1) @@ -724,7 +747,9 @@ func TestSetCellStyleNumberFormat(t *testing.T) { t.FailNow() } assert.NoError(t, f.SetCellStyle("Sheet2", c, c, style)) - t.Log(f.GetCellValue("Sheet2", c)) + cellValue, err := f.GetCellValue("Sheet2", c) + assert.Equal(t, expected[i][k], cellValue) + assert.NoError(t, err) } } var style int diff --git a/rows.go b/rows.go index 9269ac6311..fdb93742e9 100644 --- a/rows.go +++ b/rows.go @@ -857,7 +857,10 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error { if end > TotalRows { return ErrMaxRows } - if styleID < 0 { + s := f.stylesReader() + s.Lock() + defer s.Unlock() + if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { return newInvalidStyleID(styleID) } ws, err := f.workSheetReader(sheet) diff --git a/rows_test.go b/rows_test.go index 829a27aef1..02e2d2071c 100644 --- a/rows_test.go +++ b/rows_test.go @@ -956,7 +956,10 @@ func TestSetRowStyle(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet1", "B2", "B2", style1)) assert.EqualError(t, f.SetRowStyle("Sheet1", 5, -1, style2), newInvalidRowNumberError(-1).Error()) assert.EqualError(t, f.SetRowStyle("Sheet1", 1, TotalRows+1, style2), ErrMaxRows.Error()) + // Test set row style with invalid style ID. assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, -1), newInvalidStyleID(-1).Error()) + // Test set row style with not exists style ID. + assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, 10), newInvalidStyleID(10).Error()) assert.EqualError(t, f.SetRowStyle("SheetN", 1, 1, style2), "sheet SheetN does not exist") assert.NoError(t, f.SetRowStyle("Sheet1", 5, 1, style2)) cellStyleID, err := f.GetCellStyle("Sheet1", "B2") diff --git a/styles.go b/styles.go index 55ee17552d..87c4863ecd 100644 --- a/styles.go +++ b/styles.go @@ -2629,6 +2629,14 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { makeContiguousColumns(ws, hRow, vRow, vCol) ws.Lock() defer ws.Unlock() + + s := f.stylesReader() + s.Lock() + defer s.Unlock() + if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { + return newInvalidStyleID(styleID) + } + for r := hRowIdx; r <= vRowIdx; r++ { for k := hColIdx; k <= vColIdx; k++ { ws.SheetData.Row[r].C[k].S = styleID diff --git a/styles_test.go b/styles_test.go index 257f98d4f2..47aee5b802 100644 --- a/styles_test.go +++ b/styles_test.go @@ -342,6 +342,10 @@ func TestSetCellStyle(t *testing.T) { f := NewFile() // Test set cell style on not exists worksheet. assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN does not exist") + // Test set cell style with invalid style ID. + assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", -1), newInvalidStyleID(-1).Error()) + // Test set cell style with not exists style ID. + assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", 10), newInvalidStyleID(10).Error()) } func TestGetStyleID(t *testing.T) { From 00470c17d95cb0f70b57b6fb0092b6f873949cd1 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 3 Sep 2022 20:16:35 +0800 Subject: [PATCH 098/213] This closes #1338, fix apply AM/PM format issue --- numfmt.go | 6 +- numfmt_test.go | 306 ++++++++++++++++++++++++------------------------- 2 files changed, 157 insertions(+), 155 deletions(-) diff --git a/numfmt.go b/numfmt.go index 56f354f1f9..fd99240b80 100644 --- a/numfmt.go +++ b/numfmt.go @@ -696,7 +696,7 @@ func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) { nextHours := nf.hoursNext(i) aps := strings.Split(nf.localAmPm(token.TValue), "/") nf.ap = aps[0] - if nextHours > 12 { + if nextHours >= 12 { nf.ap = aps[1] } } @@ -777,9 +777,11 @@ func (nf *numberFormat) hoursHandler(i int, token nfp.Token) { ap, ok := nf.apNext(i) if ok { nf.ap = ap[0] + if h >= 12 { + nf.ap = ap[1] + } if h > 12 { h -= 12 - nf.ap = ap[1] } } if nf.ap != "" && nf.hoursNext(i) == -1 && h > 12 { diff --git a/numfmt_test.go b/numfmt_test.go index 5cdf56bc4a..f45307d517 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -76,108 +76,108 @@ func TestNumFmt(t *testing.T) { {"44682.18957170139", "[$-36]mmm dd yyyy h:mm AM/PM", "Mei 01 2022 4:32 vm."}, {"44682.18957170139", "[$-36]mmmm dd yyyy h:mm AM/PM", "Mei 01 2022 4:32 vm."}, {"44682.18957170139", "[$-36]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 vm."}, - {"43543.503206018519", "[$-445]mmm dd yyyy h:mm AM/PM", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-445]mmmm dd yyyy h:mm AM/PM", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-445]mmmmm dd yyyy h:mm AM/PM", "\u09AE 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-4]mmmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-4]mmmmm dd yyyy h:mm AM/PM", "三 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-7804]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-7804]mmmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-7804]mmmmm dd yyyy h:mm AM/PM", "三 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-804]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-804]mmmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-804]mmmmm dd yyyy h:mm AM/PM", "三 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-1004]mmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-1004]mmmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-1004]mmmmm dd yyyy h:mm AM/PM", "三 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-7C04]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-7C04]mmmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-7C04]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-C04]mmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-C04]mmmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-C04]mmmmm dd yyyy h:mm AM/PM", "三 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-1404]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-1404]mmmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-1404]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-404]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-404]mmmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-404]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 上午"}, - {"43543.503206018519", "[$-9]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-9]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-9]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1000]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1000]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1000]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-C09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 am"}, - {"43543.503206018519", "[$-C09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 am"}, - {"43543.503206018519", "[$-C09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 am"}, - {"43543.503206018519", "[$-c09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 am"}, - {"43543.503206018519", "[$-c09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 am"}, - {"43543.503206018519", "[$-c09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 am"}, - {"43543.503206018519", "[$-2829]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-2829]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-2829]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1009]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1009]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1009]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-2409]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-2409]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-2409]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-3C09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-3C09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-3C09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4009]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4009]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4009]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1809]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 am"}, - {"43543.503206018519", "[$-1809]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 am"}, - {"43543.503206018519", "[$-1809]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 am"}, - {"43543.503206018519", "[$-2009]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-2009]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-2009]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4409]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4409]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4409]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1409]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1409]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1409]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-3409]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-3409]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-3409]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4809]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4809]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4809]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1C09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1C09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-1C09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-2C09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-2C09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-2C09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4C09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4C09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-4C09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-809]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 am"}, - {"43543.503206018519", "[$-809]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 am"}, - {"43543.503206018519", "[$-809]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 am"}, - {"43543.503206018519", "[$-3009]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-3009]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-3009]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, + {"43543.503206018519", "[$-445]mmm dd yyyy h:mm AM/PM", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-445]mmmm dd yyyy h:mm AM/PM", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-445]mmmmm dd yyyy h:mm AM/PM", "\u09AE 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-4]mmmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-4]mmmmm dd yyyy h:mm AM/PM", "三 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-7804]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-7804]mmmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-7804]mmmmm dd yyyy h:mm AM/PM", "三 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-804]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-804]mmmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-804]mmmmm dd yyyy h:mm AM/PM", "三 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-1004]mmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-1004]mmmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-1004]mmmmm dd yyyy h:mm AM/PM", "三 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-7C04]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-7C04]mmmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-7C04]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-C04]mmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-C04]mmmm dd yyyy h:mm AM/PM", "三月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-C04]mmmmm dd yyyy h:mm AM/PM", "三 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-1404]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-1404]mmmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-1404]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-404]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-404]mmmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-404]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 下午"}, + {"43543.503206018519", "[$-9]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-9]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-9]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1000]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1000]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1000]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-C09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-C09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-C09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-c09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-c09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-c09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-2829]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2829]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2829]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1009]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1009]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1009]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2409]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2409]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2409]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3C09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3C09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3C09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4009]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4009]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4009]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1809]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-1809]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-1809]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-2009]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2009]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2009]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4409]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4409]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4409]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1409]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1409]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1409]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3409]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3409]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3409]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4809]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4809]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4809]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2C09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2C09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2C09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4C09]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4C09]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4C09]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-809]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-809]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-809]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 pm"}, + {"43543.503206018519", "[$-3009]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3009]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3009]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, {"44562.189571759256", "[$-C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-7]mmm dd yyyy h:mm AM/PM", "Mär 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-7]mmmm dd yyyy h:mm AM/PM", "März 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-7]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, + {"43543.503206018519", "[$-C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7]mmm dd yyyy h:mm AM/PM", "Mär 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7]mmmm dd yyyy h:mm AM/PM", "März 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, {"44562.189571759256", "[$-C07]mmm dd yyyy h:mm AM/PM", "Jän 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C07]mmmm dd yyyy h:mm AM/PM", "Jänner 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C07]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-407]mmm dd yyyy h:mm AM/PM", "Mär 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-407]mmmm dd yyyy h:mm AM/PM", "März 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-407]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 AM"}, + {"43543.503206018519", "[$-407]mmm dd yyyy h:mm AM/PM", "Mär 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-407]mmmm dd yyyy h:mm AM/PM", "März 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-407]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, {"44562.189571759256", "[$-83C]mmm dd yyyy h:mm AM/PM", "Ean 01 2022 4:32 r.n."}, {"44593.189571759256", "[$-83C]mmm dd yyyy h:mm AM/PM", "Feabh 01 2022 4:32 r.n."}, {"44621.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."}, @@ -238,21 +238,21 @@ func TestNumFmt(t *testing.T) { {"44835.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "D 01 2022 4:32 r.n."}, {"44866.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 r.n."}, {"44896.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 r.n."}, - {"43543.503206018519", "[$-10]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-10]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-10]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-11]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 午前"}, - {"43543.503206018519", "[$-11]mmmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 午前"}, - {"43543.503206018519", "[$-11]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 午前"}, - {"43543.503206018519", "[$-411]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 午前"}, - {"43543.503206018519", "[$-411]mmmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 午前"}, - {"43543.503206018519", "[$-411]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 午前"}, - {"43543.503206018519", "[$-12]mmm dd yyyy h:mm AM/PM", "3월 19 2019 12:04 오전"}, - {"43543.503206018519", "[$-12]mmmm dd yyyy h:mm AM/PM", "3월 19 2019 12:04 오전"}, - {"43543.503206018519", "[$-12]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 오전"}, - {"43543.503206018519", "[$-412]mmm dd yyyy h:mm AM/PM", "3월 19 2019 12:04 오전"}, - {"43543.503206018519", "[$-412]mmmm dd yyyy h:mm AM/PM", "3월 19 2019 12:04 오전"}, - {"43543.503206018519", "[$-412]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 오전"}, + {"43543.503206018519", "[$-10]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-10]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-10]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-11]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 午後"}, + {"43543.503206018519", "[$-11]mmmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 午後"}, + {"43543.503206018519", "[$-11]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 午後"}, + {"43543.503206018519", "[$-411]mmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 午後"}, + {"43543.503206018519", "[$-411]mmmm dd yyyy h:mm AM/PM", "3月 19 2019 12:04 午後"}, + {"43543.503206018519", "[$-411]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 午後"}, + {"43543.503206018519", "[$-12]mmm dd yyyy h:mm AM/PM", "3월 19 2019 12:04 오후"}, + {"43543.503206018519", "[$-12]mmmm dd yyyy h:mm AM/PM", "3월 19 2019 12:04 오후"}, + {"43543.503206018519", "[$-12]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 오후"}, + {"43543.503206018519", "[$-412]mmm dd yyyy h:mm AM/PM", "3월 19 2019 12:04 오후"}, + {"43543.503206018519", "[$-412]mmmm dd yyyy h:mm AM/PM", "3월 19 2019 12:04 오후"}, + {"43543.503206018519", "[$-412]mmmmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 오후"}, {"44562.189571759256", "[$-7C50]mmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"}, {"44896.18957170139", "[$-7C50]mmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C50]mmmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"}, @@ -274,78 +274,78 @@ func TestNumFmt(t *testing.T) { {"44562.189571759256", "[$-19]mmm dd yyyy h:mm AM/PM", "янв. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-19]mmmm dd yyyy h:mm AM/PM", "январь 01 2022 4:32 AM"}, {"44562.189571759256", "[$-19]mmmmm dd yyyy h:mm AM/PM", "я 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-19]mmm dd yyyy h:mm AM/PM", "март 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-19]mmmm dd yyyy h:mm AM/PM", "март 19 2019 12:04 AM"}, - {"43543.503206018519", "[$-19]mmmmm dd yyyy h:mm AM/PM", "м 19 2019 12:04 AM"}, + {"43543.503206018519", "[$-19]mmm dd yyyy h:mm AM/PM", "март 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-19]mmmm dd yyyy h:mm AM/PM", "март 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-19]mmmmm dd yyyy h:mm AM/PM", "м 19 2019 12:04 PM"}, {"44562.189571759256", "[$-A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, - {"43543.503206018519", "[$-A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, + {"43543.503206018519", "[$-A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, {"44562.189571759256", "[$-A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, - {"43543.503206018519", "[$-A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, + {"43543.503206018519", "[$-A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, {"44562.189571759256", "[$-A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, - {"43543.503206018519", "[$-A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, + {"43543.503206018519", "[$-A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, {"44562.189571759256", "[$-2C0A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-2C0A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-2C0A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, - {"43543.503206018519", "[$-2C0A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-2C0A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-2C0A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, + {"43543.503206018519", "[$-2C0A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-2C0A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-2C0A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, {"44562.189571759256", "[$-200A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-200A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-200A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, - {"43543.503206018519", "[$-200A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-200A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-200A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, + {"43543.503206018519", "[$-200A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-200A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-200A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, {"44562.189571759256", "[$-400A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-400A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-400A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, - {"43543.503206018519", "[$-400A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-400A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-400A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, + {"43543.503206018519", "[$-400A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-400A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-400A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, {"44562.189571759256", "[$-340A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-340A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-340A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, - {"43543.503206018519", "[$-340A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-340A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-340A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, + {"43543.503206018519", "[$-340A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-340A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-340A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, {"44562.189571759256", "[$-240A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-240A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-240A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, - {"43543.503206018519", "[$-240A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-240A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-240A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, + {"43543.503206018519", "[$-240A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-240A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-240A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, {"44562.189571759256", "[$-140A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-140A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, {"44562.189571759256", "[$-140A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, - {"43543.503206018519", "[$-140A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-140A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-140A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, + {"43543.503206018519", "[$-140A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-140A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-140A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, {"44562.189571759256", "[$-5C0A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-5C0A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-5C0A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.m."}, - {"43543.503206018519", "[$-5C0A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a.m."}, - {"43543.503206018519", "[$-5C0A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a.m."}, - {"43543.503206018519", "[$-5C0A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a.m."}, - {"43543.503206018519", "[$-1C0A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-1C0A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-1C0A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-300A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-300A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-300A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-440A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-440A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 a. m."}, - {"43543.503206018519", "[$-440A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 a. m."}, + {"43543.503206018519", "[$-5C0A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-5C0A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-5C0A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-1C0A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-1C0A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-1C0A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-300A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-300A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-300A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-440A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-440A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-440A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, {"44562.189571759256", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e04. 01 2022 4:32 AM"}, {"44593.189571759256", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e18. 01 2022 4:32 AM"}, {"44621.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e04. 01 2022 4:32 AM"}, From 961a3e89330ab2cd5257e04384813a7e53ea3744 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 6 Sep 2022 14:38:09 +0800 Subject: [PATCH 099/213] Fix get image content was empty after inserting image --- picture.go | 3 --- picture_test.go | 30 +++++++++++++----------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/picture.go b/picture.go index d087c61e28..07d18ccce9 100644 --- a/picture.go +++ b/picture.go @@ -505,9 +505,6 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { } target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingXML := strings.ReplaceAll(target, "..", "xl") - if _, ok := f.Pkg.Load(drawingXML); !ok { - return "", nil, err - } drawingRelationships := strings.ReplaceAll( strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") diff --git a/picture_test.go b/picture_test.go index 3ab1cc0d18..3588218627 100644 --- a/picture_test.go +++ b/picture_test.go @@ -8,7 +8,6 @@ import ( _ "image/png" "io" "io/ioutil" - "os" "path/filepath" "strings" "testing" @@ -34,9 +33,7 @@ func BenchmarkAddPictureFromBytes(b *testing.B) { func TestAddPicture(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) // Test add picture to worksheet with offset and location hyperlink. assert.NoError(t, f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), @@ -77,21 +74,14 @@ func TestAddPictureErrors(t *testing.T) { assert.NoError(t, err) // Test add picture to worksheet with invalid file path. - err = f.AddPicture("Sheet1", "G21", filepath.Join("test", "not_exists_dir", "not_exists.icon"), "") - if assert.Error(t, err) { - assert.True(t, os.IsNotExist(err), "Expected os.IsNotExist(err) == true") - } + assert.Error(t, f.AddPicture("Sheet1", "G21", filepath.Join("test", "not_exists_dir", "not_exists.icon"), "")) // Test add picture to worksheet with unsupported file type. - err = f.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), "") - assert.EqualError(t, err, ErrImgExt.Error()) - - err = f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)) - assert.EqualError(t, err, ErrImgExt.Error()) + assert.EqualError(t, f.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), ""), ErrImgExt.Error()) + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)), ErrImgExt.Error()) // Test add picture to worksheet with invalid file data. - err = f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)) - assert.EqualError(t, err, image.ErrFormat.Error()) + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)), image.ErrFormat.Error()) // Test add picture with custom image decoder and encoder. decode := func(r io.Reader) (image.Image, error) { return nil, nil } @@ -109,7 +99,14 @@ func TestAddPictureErrors(t *testing.T) { } func TestGetPicture(t *testing.T) { - f, err := prepareTestBook1() + f := NewFile() + assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), "")) + name, content, err := f.GetPicture("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, 13233, len(content)) + assert.Equal(t, "image1.png", name) + + f, err = prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } @@ -118,7 +115,6 @@ func TestGetPicture(t *testing.T) { assert.NoError(t, err) if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) || !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0o644)) { - t.FailNow() } From 0c5cdfec1868f31f6e355cdcb0a91220bad80522 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 7 Sep 2022 00:18:16 +0800 Subject: [PATCH 100/213] This closes #1293, add new function `GetColStyle` - Fix generate workbook corruption after insert cols/rows in some case - Update unit tests - Update dependencies module --- adjust.go | 28 +++++++++++++++++++--------- col.go | 26 ++++++++++++++++++++++++++ col_test.go | 20 +++++++++++++++++++- go.mod | 4 ++-- go.sum | 8 ++++---- 5 files changed, 70 insertions(+), 16 deletions(-) diff --git a/adjust.go b/adjust.go index fd570bb6a2..5f4ee3d55a 100644 --- a/adjust.go +++ b/adjust.go @@ -73,14 +73,19 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) // adjustColDimensions provides a function to update column dimensions when // inserting or deleting rows or columns. func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) error { + for rowIdx := range ws.SheetData.Row { + for _, v := range ws.SheetData.Row[rowIdx].C { + if cellCol, _, _ := CellNameToCoordinates(v.R); col <= cellCol { + if newCol := cellCol + offset; newCol > 0 && newCol > MaxColumns { + return ErrColumnNumber + } + } + } + } for rowIdx := range ws.SheetData.Row { for colIdx, v := range ws.SheetData.Row[rowIdx].C { - cellCol, cellRow, _ := CellNameToCoordinates(v.R) - if col <= cellCol { + if cellCol, cellRow, _ := CellNameToCoordinates(v.R); col <= cellCol { if newCol := cellCol + offset; newCol > 0 { - if newCol > MaxColumns { - return ErrColumnNumber - } ws.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow) } } @@ -92,12 +97,17 @@ func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) error { // adjustRowDimensions provides a function to update row dimensions when // inserting or deleting rows or columns. func (f *File) adjustRowDimensions(ws *xlsxWorksheet, row, offset int) error { - for i := range ws.SheetData.Row { + totalRows := len(ws.SheetData.Row) + if totalRows == 0 { + return nil + } + lastRow := &ws.SheetData.Row[totalRows-1] + if newRow := lastRow.R + offset; lastRow.R >= row && newRow > 0 && newRow >= TotalRows { + return ErrMaxRows + } + for i := 0; i < len(ws.SheetData.Row); i++ { r := &ws.SheetData.Row[i] if newRow := r.R + offset; r.R >= row && newRow > 0 { - if newRow >= TotalRows { - return ErrMaxRows - } f.adjustSingleRowDimensions(r, newRow) } } diff --git a/col.go b/col.go index c0deb58cc0..b998f654a0 100644 --- a/col.go +++ b/col.go @@ -638,6 +638,30 @@ func (f *File) getColWidth(sheet string, col int) int { return int(defaultColWidthPixels) } +// GetColStyle provides a function to get column style ID by given worksheet +// name and column name. +func (f *File) GetColStyle(sheet, col string) (int, error) { + var styleID int + colNum, err := ColumnNameToNumber(col) + if err != nil { + return styleID, err + } + ws, err := f.workSheetReader(sheet) + if err != nil { + return styleID, err + } + ws.Lock() + defer ws.Unlock() + if ws.Cols != nil { + for _, v := range ws.Cols.Col { + if v.Min <= colNum && colNum <= v.Max { + styleID = v.Style + } + } + } + return styleID, err +} + // GetColWidth provides a function to get column width by given worksheet name // and column name. func (f *File) GetColWidth(sheet, col string) (float64, error) { @@ -649,6 +673,8 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { if err != nil { return defaultColWidth, err } + ws.Lock() + defer ws.Unlock() if ws.Cols != nil { var width float64 for _, v := range ws.Cols.Col { diff --git a/col_test.go b/col_test.go index 1076f31c72..f01ffdc509 100644 --- a/col_test.go +++ b/col_test.go @@ -293,7 +293,7 @@ func TestSetColStyle(t *testing.T) { assert.NoError(t, err) // Test set column style on not exists worksheet. assert.EqualError(t, f.SetColStyle("SheetN", "E", styleID), "sheet SheetN does not exist") - // Test set column style with illegal cell coordinates. + // Test set column style with illegal column name. assert.EqualError(t, f.SetColStyle("Sheet1", "*", styleID), newInvalidColumnNameError("*").Error()) assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", styleID), newInvalidColumnNameError("*").Error()) // Test set column style with invalid style ID. @@ -302,6 +302,10 @@ func TestSetColStyle(t *testing.T) { assert.EqualError(t, f.SetColStyle("Sheet1", "B", 10), newInvalidStyleID(10).Error()) assert.NoError(t, f.SetColStyle("Sheet1", "B", styleID)) + style, err := f.GetColStyle("Sheet1", "B") + assert.NoError(t, err) + assert.Equal(t, styleID, style) + // Test set column style with already exists column with style. assert.NoError(t, f.SetColStyle("Sheet1", "B", styleID)) assert.NoError(t, f.SetColStyle("Sheet1", "D:C", styleID)) @@ -343,6 +347,20 @@ func TestColWidth(t *testing.T) { convertRowHeightToPixels(0) } +func TestGetColStyle(t *testing.T) { + f := NewFile() + styleID, err := f.GetColStyle("Sheet1", "A") + assert.NoError(t, err) + assert.Equal(t, styleID, 0) + + // Test set column style on not exists worksheet. + _, err = f.GetColStyle("SheetN", "A") + assert.EqualError(t, err, "sheet SheetN does not exist") + // Test set column style with illegal column name. + _, err = f.GetColStyle("Sheet1", "*") + assert.EqualError(t, err, newInvalidColumnNameError("*").Error()) +} + func TestInsertCols(t *testing.T) { f := NewFile() sheet1 := f.GetSheetName(0) diff --git a/go.mod b/go.mod index b03e25465e..9d49dbee0a 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,9 @@ require ( github.com/stretchr/testify v1.7.1 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-20220817201139-bc19a97f63c8 + golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 - golang.org/x/net v0.0.0-20220812174116-3211cb980234 + golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index 9512add09e..3f9cd78d3d 100644 --- a/go.sum +++ b/go.sum @@ -17,13 +17,13 @@ github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj0 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-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUHW7cJMmx3TGZOrnyYaNQ6c= -golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/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-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E= -golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 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= From fb1aab7add52808c96c9cc10570fe73ce797b7f4 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 8 Sep 2022 22:20:21 +0800 Subject: [PATCH 101/213] This closes #744, the `Save`, `Write` and `WriteTo` function accept saving options --- crypt_test.go | 2 ++ excelize.go | 4 ++-- excelize_test.go | 9 +++------ file.go | 42 ++++++++++++++++++++---------------------- file_test.go | 8 ++++++++ xmlDrawing.go | 9 +++++++++ 6 files changed, 44 insertions(+), 30 deletions(-) diff --git a/crypt_test.go b/crypt_test.go index f7c465ed09..95b6f52cc4 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -48,6 +48,8 @@ func TestEncrypt(t *testing.T) { cell, err = f.GetCellValue("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, "SECRET", cell) + // Test remove password by save workbook with options + assert.NoError(t, f.Save(Options{Password: ""})) assert.NoError(t, f.Close()) } diff --git a/excelize.go b/excelize.go index f1269fef68..bb4bde063a 100644 --- a/excelize.go +++ b/excelize.go @@ -136,13 +136,13 @@ func newFile() *File { // OpenReader read data stream from io.Reader and return a populated // spreadsheet file. -func OpenReader(r io.Reader, opt ...Options) (*File, error) { +func OpenReader(r io.Reader, opts ...Options) (*File, error) { b, err := ioutil.ReadAll(r) if err != nil { return nil, err } f := newFile() - f.options = parseOptions(opt...) + f.options = parseOptions(opts...) if f.options.UnzipSizeLimit == 0 { f.options.UnzipSizeLimit = UnzipSizeLimit if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit { diff --git a/excelize_test.go b/excelize_test.go index 19aba7eeaf..93cd2bf5bb 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -186,18 +186,15 @@ func TestOpenFile(t *testing.T) { func TestSaveFile(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.EqualError(t, f.SaveAs(filepath.Join("test", "TestSaveFile.xlsb")), ErrWorkbookFileFormat.Error()) for _, ext := range []string{".xlam", ".xlsm", ".xlsx", ".xltm", ".xltx"} { assert.NoError(t, f.SaveAs(filepath.Join("test", fmt.Sprintf("TestSaveFile%s", ext)))) } assert.NoError(t, f.Close()) + f, err = OpenFile(filepath.Join("test", "TestSaveFile.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.Save()) assert.NoError(t, f.Close()) } diff --git a/file.go b/file.go index 065e7c5a52..c83d17efc4 100644 --- a/file.go +++ b/file.go @@ -55,44 +55,32 @@ func NewFile() *File { } // Save provides a function to override the spreadsheet with origin path. -func (f *File) Save() error { +func (f *File) Save(opts ...Options) error { if f.Path == "" { return ErrSave } - if f.options != nil { - return f.SaveAs(f.Path, *f.options) + for i := range opts { + f.options = &opts[i] } - return f.SaveAs(f.Path) + return f.SaveAs(f.Path, *f.options) } // SaveAs provides a function to create or update to a spreadsheet at the // provided path. -func (f *File) SaveAs(name string, opt ...Options) error { +func (f *File) SaveAs(name string, opts ...Options) error { if len(name) > MaxFilePathLength { return ErrMaxFilePathLength } f.Path = name - contentType, ok := map[string]string{ - ".xlam": ContentTypeAddinMacro, - ".xlsm": ContentTypeMacro, - ".xlsx": ContentTypeSheetML, - ".xltm": ContentTypeTemplateMacro, - ".xltx": ContentTypeTemplate, - }[filepath.Ext(f.Path)] - if !ok { + if _, ok := supportedContentType[filepath.Ext(f.Path)]; !ok { return ErrWorkbookFileFormat } - f.setContentTypePartProjectExtensions(contentType) file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) if err != nil { return err } defer file.Close() - f.options = nil - for i := range opt { - f.options = &opt[i] - } - return f.Write(file) + return f.Write(file, opts...) } // Close closes and cleanup the open temporary file for the spreadsheet. @@ -113,13 +101,23 @@ func (f *File) Close() error { } // Write provides a function to write to an io.Writer. -func (f *File) Write(w io.Writer) error { - _, err := f.WriteTo(w) +func (f *File) Write(w io.Writer, opts ...Options) error { + _, err := f.WriteTo(w, opts...) return err } // WriteTo implements io.WriterTo to write the file. -func (f *File) WriteTo(w io.Writer) (int64, error) { +func (f *File) WriteTo(w io.Writer, opts ...Options) (int64, error) { + for i := range opts { + f.options = &opts[i] + } + if len(f.Path) != 0 { + contentType, ok := supportedContentType[filepath.Ext(f.Path)] + if !ok { + return 0, ErrWorkbookFileFormat + } + f.setContentTypePartProjectExtensions(contentType) + } if f.options != nil && f.options.Password != "" { buf, err := f.WriteToBuffer() if err != nil { diff --git a/file_test.go b/file_test.go index 8e65c5d46d..83a9b786f6 100644 --- a/file_test.go +++ b/file_test.go @@ -71,6 +71,14 @@ func TestWriteTo(t *testing.T) { _, err := f.WriteTo(bufio.NewWriter(&buf)) assert.EqualError(t, err, "zip: FileHeader.Name too long") } + // Test write with unsupported workbook file format + { + f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{} + f.Pkg.Store("/d", []byte("s")) + f.Path = "Book1.xls" + _, err := f.WriteTo(bufio.NewWriter(&buf)) + assert.EqualError(t, err, ErrWorkbookFileFormat.Error()) + } } func TestClose(t *testing.T) { diff --git a/xmlDrawing.go b/xmlDrawing.go index 34c9858382..fc8dee5890 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -144,6 +144,15 @@ const ( // supportedImageTypes defined supported image types. var supportedImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff", ".emf": ".emf", ".wmf": ".wmf", ".emz": ".emz", ".wmz": ".wmz"} +// supportedContentType defined supported file format types. +var supportedContentType = map[string]string{ + ".xlam": ContentTypeAddinMacro, + ".xlsm": ContentTypeMacro, + ".xlsx": ContentTypeSheetML, + ".xltm": ContentTypeTemplateMacro, + ".xltx": ContentTypeTemplate, +} + // xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This // element specifies non-visual canvas properties. This allows for additional // information that does not affect the appearance of the picture to be stored. From c72fb747b8a64117538229f1e5a85d220349b6f1 Mon Sep 17 00:00:00 2001 From: dafengge0913 Date: Sat, 10 Sep 2022 13:05:34 +0800 Subject: [PATCH 102/213] Fix DeleteComment slice bounds out of range (#1343) --- comment.go | 23 +++++++++++++---------- comment_test.go | 6 +++++- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/comment.go b/comment.go index ac22ec7476..a75ea7f4cb 100644 --- a/comment.go +++ b/comment.go @@ -156,17 +156,20 @@ func (f *File) DeleteComment(sheet, cell string) (err error) { } commentsXML = strings.TrimPrefix(commentsXML, "/") if comments := f.commentsReader(commentsXML); comments != nil { - for i, cmt := range comments.CommentList.Comment { - if cmt.Ref == cell { - if len(comments.CommentList.Comment) > 1 { - comments.CommentList.Comment = append( - comments.CommentList.Comment[:i], - comments.CommentList.Comment[i+1:]..., - ) - continue - } - comments.CommentList.Comment = nil + for i := 0; i < len(comments.CommentList.Comment); i++ { + cmt := comments.CommentList.Comment[i] + if cmt.Ref != cell { + continue + } + if len(comments.CommentList.Comment) > 1 { + comments.CommentList.Comment = append( + comments.CommentList.Comment[:i], + comments.CommentList.Comment[i+1:]..., + ) + i-- + continue } + comments.CommentList.Comment = nil } f.Comments[commentsXML] = comments } diff --git a/comment_test.go b/comment_test.go index 64e9968e76..0d1e039e4b 100644 --- a/comment_test.go +++ b/comment_test.go @@ -55,15 +55,19 @@ func TestDeleteComment(t *testing.T) { assert.NoError(t, f.AddComment("Sheet2", "A40", `{"author":"Excelize: ","text":"This is a comment1."}`)) assert.NoError(t, f.AddComment("Sheet2", "A41", `{"author":"Excelize: ","text":"This is a comment2."}`)) assert.NoError(t, f.AddComment("Sheet2", "C41", `{"author":"Excelize: ","text":"This is a comment3."}`)) + assert.NoError(t, f.AddComment("Sheet2", "C41", `{"author":"Excelize: ","text":"This is a comment3-1."}`)) + assert.NoError(t, f.AddComment("Sheet2", "C42", `{"author":"Excelize: ","text":"This is a comment4."}`)) + assert.NoError(t, f.AddComment("Sheet2", "C41", `{"author":"Excelize: ","text":"This is a comment3-2."}`)) assert.NoError(t, f.DeleteComment("Sheet2", "A40")) - assert.EqualValues(t, 2, len(f.GetComments()["Sheet2"])) + assert.EqualValues(t, 5, len(f.GetComments()["Sheet2"])) assert.EqualValues(t, len(NewFile().GetComments()), 0) // Test delete all comments in a worksheet assert.NoError(t, f.DeleteComment("Sheet2", "A41")) assert.NoError(t, f.DeleteComment("Sheet2", "C41")) + assert.NoError(t, f.DeleteComment("Sheet2", "C42")) assert.EqualValues(t, 0, len(f.GetComments()["Sheet2"])) // Test delete comment on not exists worksheet assert.EqualError(t, f.DeleteComment("SheetN", "A1"), "sheet SheetN does not exist") From b6cc43d8242fd3f7f0c6163db9fcd759b9b992b1 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 11 Sep 2022 00:04:04 +0800 Subject: [PATCH 103/213] This makes 6 functions concurrency safety - These 6 functions now support concurrency safe: SetColWidth, GetColWidth, SetColVisible, GetColVisible, SetColStyle and GetColStyle --- cell.go | 15 ++++++++------- cell_test.go | 19 ++++++++++++++++++- col.go | 31 +++++++++++++++++++++---------- excelize_test.go | 22 +++++++++++++++++----- merge.go | 4 ++++ picture.go | 4 ++-- rows.go | 3 ++- styles.go | 13 +++++++------ 8 files changed, 79 insertions(+), 32 deletions(-) diff --git a/cell.go b/cell.go index dd6b1695f1..251cab85af 100644 --- a/cell.go +++ b/cell.go @@ -60,7 +60,7 @@ var cellTypes = map[string]CellType{ // worksheet name and axis in spreadsheet file. If it is possible to apply a // format to the cell value, it will do so, if not then an error will be // returned, along with the raw value of the cell. All cells' values will be -// the same in a merged range. +// the same in a merged range. This function is concurrency safe. func (f *File) GetCellValue(sheet, axis string, opts ...Options) (string, error) { return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { val, err := c.getValueFrom(f, f.sharedStringsReader(), parseOptions(opts...).RawCellValue) @@ -85,10 +85,10 @@ func (f *File) GetCellType(sheet, axis string) (CellType, error) { return cellType, err } -// SetCellValue provides a function to set the value of a cell. The specified -// coordinates should not be in the first row of the table, a complex number -// can be set with string text. The following shows the supported data -// types: +// SetCellValue provides a function to set the value of a cell. This function +// is concurrency safe. The specified coordinates should not be in the first +// row of the table, a complex number can be set with string text. The +// following shows the supported data types: // // int // int8 @@ -1047,8 +1047,9 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { } // SetSheetRow writes an array to row by given worksheet name, starting -// coordinate and a pointer to array type 'slice'. For example, writes an -// array to row 6 start with the cell B6 on Sheet1: +// coordinate and a pointer to array type 'slice'. This function is +// concurrency safe. For example, writes an array to row 6 start with the cell +// B6 on Sheet1: // // err := f.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { diff --git a/cell_test.go b/cell_test.go index 45970fcb89..5b8e639a79 100644 --- a/cell_test.go +++ b/cell_test.go @@ -64,7 +64,24 @@ func TestConcurrency(t *testing.T) { _, err := cols.Rows() assert.NoError(t, err) } - + // Concurrency set columns style + assert.NoError(t, f.SetColStyle("Sheet1", "C:E", style)) + // Concurrency get columns style + styleID, err := f.GetColStyle("Sheet1", "D") + assert.NoError(t, err) + assert.Equal(t, style, styleID) + // Concurrency set columns width + assert.NoError(t, f.SetColWidth("Sheet1", "A", "B", 10)) + // Concurrency get columns width + width, err := f.GetColWidth("Sheet1", "A") + assert.NoError(t, err) + assert.Equal(t, 10.0, width) + // Concurrency set columns visible + assert.NoError(t, f.SetColVisible("Sheet1", "A:B", true)) + // Concurrency get columns visible + visible, err := f.GetColVisible("Sheet1", "A") + assert.NoError(t, err) + assert.Equal(t, true, visible) wg.Done() }(i, t) } diff --git a/col.go b/col.go index b998f654a0..adc7f85ea6 100644 --- a/col.go +++ b/col.go @@ -184,7 +184,8 @@ func columnXMLHandler(colIterator *columnXMLIterator, xmlElement *xml.StartEleme } // Cols returns a columns iterator, used for streaming reading data for a -// worksheet with a large data. For example: +// worksheet with a large data. This function is concurrency safe. For +// example: // // cols, err := f.Cols("Sheet1") // if err != nil { @@ -239,8 +240,8 @@ func (f *File) Cols(sheet string) (*Cols, error) { } // GetColVisible provides a function to get visible of a single column by given -// worksheet name and column name. For example, get visible state of column D -// in Sheet1: +// worksheet name and column name. This function is concurrency safe. For +// example, get visible state of column D in Sheet1: // // visible, err := f.GetColVisible("Sheet1", "D") func (f *File) GetColVisible(sheet, col string) (bool, error) { @@ -252,6 +253,8 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { if err != nil { return false, err } + ws.Lock() + defer ws.Unlock() if ws.Cols == nil { return true, err } @@ -266,7 +269,7 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { } // SetColVisible provides a function to set visible columns by given worksheet -// name, columns range and visibility. +// name, columns range and visibility. This function is concurrency safe. // // For example hide column D on Sheet1: // @@ -284,6 +287,8 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error { if err != nil { return err } + ws.Lock() + defer ws.Unlock() colData := xlsxCol{ Min: start, Max: end, @@ -399,9 +404,9 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { } // SetColStyle provides a function to set style of columns by given worksheet -// name, columns range and style ID. Note that this will overwrite the -// existing styles for the columns, it won't append or merge style with -// existing styles. +// name, columns range and style ID. This function is concurrency safe. Note +// that this will overwrite the existing styles for the columns, it won't +// append or merge style with existing styles. // // For example set style of column H on Sheet1: // @@ -426,6 +431,7 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { if err != nil { return err } + ws.Lock() if ws.Cols == nil { ws.Cols = &xlsxCols{} } @@ -444,6 +450,7 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { fc.Width = c.Width return fc }) + ws.Unlock() if rows := len(ws.SheetData.Row); rows > 0 { for col := start; col <= end; col++ { from, _ := CoordinatesToCellName(col, 1) @@ -455,7 +462,7 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { } // SetColWidth provides a function to set the width of a single column or -// multiple columns. For example: +// multiple columns. This function is concurrency safe. For example: // // f := excelize.NewFile() // err := f.SetColWidth("Sheet1", "A", "H", 20) @@ -479,6 +486,8 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error if err != nil { return err } + ws.Lock() + defer ws.Unlock() col := xlsxCol{ Min: min, Max: max, @@ -623,6 +632,8 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh // sheet name and column number. func (f *File) getColWidth(sheet string, col int) int { ws, _ := f.workSheetReader(sheet) + ws.Lock() + defer ws.Unlock() if ws.Cols != nil { var width float64 for _, v := range ws.Cols.Col { @@ -639,7 +650,7 @@ func (f *File) getColWidth(sheet string, col int) int { } // GetColStyle provides a function to get column style ID by given worksheet -// name and column name. +// name and column name. This function is concurrency safe. func (f *File) GetColStyle(sheet, col string) (int, error) { var styleID int colNum, err := ColumnNameToNumber(col) @@ -663,7 +674,7 @@ func (f *File) GetColStyle(sheet, col string) (int, error) { } // GetColWidth provides a function to get column width by given worksheet name -// and column name. +// and column name. This function is concurrency safe. func (f *File) GetColWidth(sheet, col string) (float64, error) { colNum, err := ColumnNameToNumber(col) if err != nil { diff --git a/excelize_test.go b/excelize_test.go index 93cd2bf5bb..9d60b1c30e 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1201,14 +1201,26 @@ func TestHSL(t *testing.T) { assert.Equal(t, 0.0, hueToRGB(0, 0, 1.0/7)) assert.Equal(t, 0.0, hueToRGB(0, 0, 0.4)) assert.Equal(t, 0.0, hueToRGB(0, 0, 2.0/4)) - t.Log(RGBToHSL(255, 255, 0)) - h, s, l := RGBToHSL(0, 255, 255) + h, s, l := RGBToHSL(255, 255, 0) + assert.Equal(t, 0.16666666666666666, h) + assert.Equal(t, 1.0, s) + assert.Equal(t, 0.5, l) + h, s, l = RGBToHSL(0, 255, 255) assert.Equal(t, 0.5, h) assert.Equal(t, 1.0, s) assert.Equal(t, 0.5, l) - t.Log(RGBToHSL(250, 100, 50)) - t.Log(RGBToHSL(50, 100, 250)) - t.Log(RGBToHSL(250, 50, 100)) + h, s, l = RGBToHSL(250, 100, 50) + assert.Equal(t, 0.041666666666666664, h) + assert.Equal(t, 0.9523809523809524, s) + assert.Equal(t, 0.5882352941176471, l) + h, s, l = RGBToHSL(50, 100, 250) + assert.Equal(t, 0.625, h) + assert.Equal(t, 0.9523809523809524, s) + assert.Equal(t, 0.5882352941176471, l) + h, s, l = RGBToHSL(250, 50, 100) + assert.Equal(t, 0.9583333333333334, h) + assert.Equal(t, 0.9523809523809524, s) + assert.Equal(t, 0.5882352941176471, l) } func TestProtectSheet(t *testing.T) { diff --git a/merge.go b/merge.go index d7400a2256..c31416a262 100644 --- a/merge.go +++ b/merge.go @@ -60,6 +60,8 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error { if err != nil { return err } + ws.Lock() + defer ws.Unlock() ref := hCell + ":" + vCell if ws.MergeCells != nil { ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect}) @@ -81,6 +83,8 @@ func (f *File) UnmergeCell(sheet string, hCell, vCell string) error { if err != nil { return err } + ws.Lock() + defer ws.Unlock() rect1, err := areaRefToCoordinates(hCell + ":" + vCell) if err != nil { return err diff --git a/picture.go b/picture.go index 07d18ccce9..30a255d4d1 100644 --- a/picture.go +++ b/picture.go @@ -39,7 +39,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // AddPicture provides the method to add picture in a sheet by given picture // format set (such as offset, scale, aspect ratio setting and print settings) -// and file path. For example: +// and file path. This function is concurrency safe. For example: // // package main // @@ -469,7 +469,7 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // GetPicture provides a function to get picture base name and raw content // embed in spreadsheet by given worksheet and cell name. This function // returns the file name in spreadsheet and file contents as []byte data -// types. For example: +// types. This function is concurrency safe. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { diff --git a/rows.go b/rows.go index fdb93742e9..561f64b243 100644 --- a/rows.go +++ b/rows.go @@ -238,7 +238,8 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta } // Rows returns a rows iterator, used for streaming reading data for a -// worksheet with a large data. For example: +// worksheet with a large data. This function is concurrency safe. For +// example: // // rows, err := f.Rows("Sheet1") // if err != nil { diff --git a/styles.go b/styles.go index 87c4863ecd..ded7c30733 100644 --- a/styles.go +++ b/styles.go @@ -1005,8 +1005,9 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { return &fs, err } -// NewStyle provides a function to create the style for cells by given JSON or -// structure pointer. Note that the color field uses RGB color code. +// NewStyle provides a function to create the style for cells by given +// structure pointer or JSON. This function is concurrency safe. Note that the +// color field uses RGB color code. // // The following shows the border styles sorted by excelize index number: // @@ -2493,10 +2494,10 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { } // SetCellStyle provides a function to add style attribute for cells by given -// worksheet name, coordinate area and style ID. Note that diagonalDown and -// diagonalUp type border should be use same color in the same coordinate -// area. SetCellStyle will overwrite the existing styles for the cell, it -// won't append or merge style with existing styles. +// worksheet name, coordinate area and style ID. This function is concurrency +// safe. Note that diagonalDown and diagonalUp type border should be use same +// color in the same coordinate area. SetCellStyle will overwrite the existing +// styles for the cell, it won't append or merge style with existing styles. // // For example create a borders of cell H9 on Sheet1: // From 73cc4bd44933994ffa8efad9c3e05fe7cb826b49 Mon Sep 17 00:00:00 2001 From: Artem Tarasenko Date: Tue, 13 Sep 2022 19:05:05 +0300 Subject: [PATCH 104/213] This closes #1345, support set custom line color in the charts (#1346) --- chart.go | 2 +- chart_test.go | 2 +- drawing.go | 13 ++++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/chart.go b/chart.go index e18545d773..5f7ae3daad 100644 --- a/chart.go +++ b/chart.go @@ -653,7 +653,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // values: This is the most important property of a series and is the only mandatory option for every chart object. This option links the chart with the worksheet data that it displays. // -// line: This sets the line format of the line chart. The line property is optional and if it isn't supplied it will default style. The options that can be set is width. The range of width is 0.25pt - 999pt. If the value of width is outside the range, the default width of the line is 2pt. +// line: This sets the line format of the line chart. The line property is optional and if it isn't supplied it will default style. The options that can be set are width and color. The range of width is 0.25pt - 999pt. If the value of width is outside the range, the default width of the line is 2pt. The value for color should be represented in hex format (e.g., #000000 - #FFFFFF) // // marker: This sets the marker of the line chart and scatter chart. The range of optional field 'size' is 2-72 (default value is 5). The enumeration value of optional field 'symbol' are (default value is 'auto'): // diff --git a/chart_test.go b/chart_test.go index 82c6903281..9184f26e36 100644 --- a/chart_test.go +++ b/chart_test.go @@ -138,7 +138,7 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.AddChart("Sheet2", "P1", `{"type":"radar","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top_right","show_legend_key":false},"title":{"name":"Radar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"span"}`)) assert.NoError(t, f.AddChart("Sheet2", "X1", `{"type":"scatter","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"Scatter Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"right","show_legend_key":false},"title":{"name":"Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero","hole_size":30}`)) - assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30","marker":{"symbol":"none","size":10}},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top","show_legend_key":false},"title":{"name":"Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true,"minor_grid_lines":true,"tick_label_skip":1},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1}}`)) + assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30","marker":{"symbol":"none","size":10}, "line":{"color":"#000000"}},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top","show_legend_key":false},"title":{"name":"Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true,"minor_grid_lines":true,"tick_label_skip":1},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1}}`)) assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"3D Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"gap"}`)) assert.NoError(t, f.AddChart("Sheet2", "P48", `{"type":"bar","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Clustered Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) diff --git a/drawing.go b/drawing.go index 59b6d2a9c8..e05c9beb8f 100644 --- a/drawing.go +++ b/drawing.go @@ -768,6 +768,16 @@ func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { // drawChartSeriesSpPr provides a function to draw the c:spPr element by given // format sets. func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { + var srgbClr *attrValString + var schemeClr *aSchemeClr + + if color := stringPtr(formatSet.Series[i].Line.Color); *color != "" { + *color = strings.TrimPrefix(*color, "#") + srgbClr = &attrValString{Val: color} + } else { + schemeClr = &aSchemeClr{Val: "accent" + strconv.Itoa((formatSet.order+i)%6+1)} + } + spPrScatter := &cSpPr{ Ln: &aLn{ W: 25400, @@ -779,7 +789,8 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { W: f.ptToEMUs(formatSet.Series[i].Line.Width), Cap: "rnd", // rnd, sq, flat SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa((formatSet.order+i)%6+1)}, + SchemeClr: schemeClr, + SrgbClr: srgbClr, }, }, } From 3f702999e6bba26afbd2a259f6849e536042ec2e Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 18 Sep 2022 00:07:15 +0800 Subject: [PATCH 105/213] Using the specialized name in a variable and making comments clear - Add JSON tags for `AppProperties`, `PivotTableOption` and `PivotTableField` structure --- adjust.go | 6 +- adjust_test.go | 6 +- calc.go | 4 +- calcchain.go | 4 +- cell.go | 271 ++++++++++++++++++++++++----------------------- cell_test.go | 4 +- chart.go | 4 +- chart_test.go | 2 +- col_test.go | 10 +- comment.go | 2 +- comment_test.go | 2 +- crypt.go | 22 ++-- excelize.go | 20 ++-- excelize_test.go | 4 +- lib.go | 22 ++-- lib_test.go | 4 +- merge.go | 26 +++-- picture.go | 4 +- picture_test.go | 6 +- pivotTable.go | 198 +++++++++++++++++----------------- rows_test.go | 2 +- sheet.go | 39 +++---- sheet_test.go | 32 +++--- sheetpr_test.go | 90 ++++++++-------- sparkline.go | 54 +++++----- stream.go | 32 +++--- stream_test.go | 4 +- styles.go | 26 ++--- table.go | 14 +-- table_test.go | 12 +-- xmlApp.go | 14 +-- 31 files changed, 470 insertions(+), 470 deletions(-) diff --git a/adjust.go b/adjust.go index 5f4ee3d55a..3a0271da5f 100644 --- a/adjust.go +++ b/adjust.go @@ -248,8 +248,8 @@ func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, off } // adjustAutoFilterHelper provides a function for adjusting auto filter to -// compare and calculate cell axis by the given adjust direction, operation -// axis and offset. +// compare and calculate cell reference by the given adjust direction, operation +// reference and offset. func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, num, offset int) []int { if dir == rows { if coordinates[1] >= num { @@ -314,7 +314,7 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off } // adjustMergeCellsHelper provides a function for adjusting merge cells to -// compare and calculate cell axis by the given pivot, operation axis and +// compare and calculate cell reference by the given pivot, operation reference and // offset. func (f *File) adjustMergeCellsHelper(p1, p2, num, offset int) (int, int) { if p2 < p1 { diff --git a/adjust_test.go b/adjust_test.go index aa374da3bf..a3e73abea8 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -10,7 +10,7 @@ import ( func TestAdjustMergeCells(t *testing.T) { f := NewFile() - // testing adjustAutoFilter with illegal cell coordinates. + // testing adjustAutoFilter with illegal cell reference. assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -283,7 +283,7 @@ func TestAdjustAutoFilter(t *testing.T) { Ref: "A1:A3", }, }, rows, 1, -1)) - // Test adjustAutoFilter with illegal cell coordinates. + // Test adjustAutoFilter with illegal cell reference. assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{ Ref: "A:B1", @@ -335,7 +335,7 @@ func TestAdjustHelper(t *testing.T) { f.Sheet.Store("xl/worksheets/sheet2.xml", &xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{Ref: "A1:B"}, }) - // Test adjustHelper with illegal cell coordinates. + // Test adjustHelper with illegal cell reference. assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) // Test adjustHelper on not exists worksheet. diff --git a/calc.go b/calc.go index f6217a8778..f7b3a6300a 100644 --- a/calc.go +++ b/calc.go @@ -1408,7 +1408,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (arg fo cr := cellRef{} if len(tokens) == 2 { // have a worksheet name cr.Sheet = tokens[0] - // cast to cell coordinates + // cast to cell reference if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[1]); err != nil { // cast to column if cr.Col, err = ColumnNameToNumber(tokens[1]); err != nil { @@ -1428,7 +1428,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (arg fo refs.PushBack(cr) continue } - // cast to cell coordinates + // cast to cell reference if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[0]); err != nil { // cast to column if cr.Col, err = ColumnNameToNumber(tokens[0]); err != nil { diff --git a/calcchain.go b/calcchain.go index 1007de145a..80928c24a7 100644 --- a/calcchain.go +++ b/calcchain.go @@ -45,11 +45,11 @@ func (f *File) calcChainWriter() { // deleteCalcChain provides a function to remove cell reference on the // calculation chain. -func (f *File) deleteCalcChain(index int, axis string) { +func (f *File) deleteCalcChain(index int, cell string) { calc := f.calcChainReader() if calc != nil { calc.C = xlsxCalcChainCollection(calc.C).Filter(func(c xlsxCalcChainC) bool { - return !((c.I == index && c.R == axis) || (c.I == index && axis == "") || (c.I == 0 && c.R == axis)) + return !((c.I == index && c.R == cell) || (c.I == index && cell == "") || (c.I == 0 && c.R == cell)) }) } if len(calc.C) == 0 { diff --git a/cell.go b/cell.go index 251cab85af..b97c4109b1 100644 --- a/cell.go +++ b/cell.go @@ -57,26 +57,27 @@ var cellTypes = map[string]CellType{ } // GetCellValue provides a function to get formatted value from cell by given -// worksheet name and axis in spreadsheet file. If it is possible to apply a -// format to the cell value, it will do so, if not then an error will be -// returned, along with the raw value of the cell. All cells' values will be -// the same in a merged range. This function is concurrency safe. -func (f *File) GetCellValue(sheet, axis string, opts ...Options) (string, error) { - return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { +// worksheet name and cell reference in spreadsheet. The return value is +// converted to the 'string' data type. This function is concurrency safe. If +// the cell format can be applied to the value of a cell, the applied value +// will be returned, otherwise the original value will be returned. All cells' +// values will be the same in a merged range. +func (f *File) GetCellValue(sheet, cell string, opts ...Options) (string, error) { + return f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { val, err := c.getValueFrom(f, f.sharedStringsReader(), parseOptions(opts...).RawCellValue) return val, true, err }) } // GetCellType provides a function to get the cell's data type by given -// worksheet name and axis in spreadsheet file. -func (f *File) GetCellType(sheet, axis string) (CellType, error) { +// worksheet name and cell reference in spreadsheet file. +func (f *File) GetCellType(sheet, cell string) (CellType, error) { var ( err error cellTypeStr string cellType CellType ) - if cellTypeStr, err = f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { + if cellTypeStr, err = f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { return c.T, true, nil }); err != nil { return CellTypeUnset, err @@ -110,39 +111,39 @@ func (f *File) GetCellType(sheet, axis string) (CellType, error) { // nil // // Note that default date format is m/d/yy h:mm of time.Time type value. You -// can set numbers format by SetCellStyle() method. If you need to set the +// can set numbers format by the SetCellStyle function. If you need to set the // specialized date in Excel like January 0, 1900 or February 29, 1900, these // times can not representation in Go language time.Time data type. Please set // the cell value as number 0 or 60, then create and bind the date-time number // format style for the cell. -func (f *File) SetCellValue(sheet, axis string, value interface{}) error { +func (f *File) SetCellValue(sheet, cell string, value interface{}) error { var err error switch v := value.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - err = f.setCellIntFunc(sheet, axis, v) + err = f.setCellIntFunc(sheet, cell, v) case float32: - err = f.SetCellFloat(sheet, axis, float64(v), -1, 32) + err = f.SetCellFloat(sheet, cell, float64(v), -1, 32) case float64: - err = f.SetCellFloat(sheet, axis, v, -1, 64) + err = f.SetCellFloat(sheet, cell, v, -1, 64) case string: - err = f.SetCellStr(sheet, axis, v) + err = f.SetCellStr(sheet, cell, v) case []byte: - err = f.SetCellStr(sheet, axis, string(v)) + err = f.SetCellStr(sheet, cell, string(v)) case time.Duration: _, d := setCellDuration(v) - err = f.SetCellDefault(sheet, axis, d) + err = f.SetCellDefault(sheet, cell, d) if err != nil { return err } - err = f.setDefaultTimeStyle(sheet, axis, 21) + err = f.setDefaultTimeStyle(sheet, cell, 21) case time.Time: - err = f.setCellTimeFunc(sheet, axis, v) + err = f.setCellTimeFunc(sheet, cell, v) case bool: - err = f.SetCellBool(sheet, axis, v) + err = f.SetCellBool(sheet, cell, v) case nil: - err = f.SetCellDefault(sheet, axis, "") + err = f.SetCellDefault(sheet, cell, "") default: - err = f.SetCellStr(sheet, axis, fmt.Sprint(value)) + err = f.SetCellStr(sheet, cell, fmt.Sprint(value)) } return err } @@ -188,58 +189,58 @@ func (f *File) removeFormula(c *xlsxC, ws *xlsxWorksheet, sheet string) { } // setCellIntFunc is a wrapper of SetCellInt. -func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error { +func (f *File) setCellIntFunc(sheet, cell string, value interface{}) error { var err error switch v := value.(type) { case int: - err = f.SetCellInt(sheet, axis, v) + err = f.SetCellInt(sheet, cell, v) case int8: - err = f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, cell, int(v)) case int16: - err = f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, cell, int(v)) case int32: - err = f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, cell, int(v)) case int64: - err = f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, cell, int(v)) case uint: - err = f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, cell, int(v)) case uint8: - err = f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, cell, int(v)) case uint16: - err = f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, cell, int(v)) case uint32: - err = f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, cell, int(v)) case uint64: - err = f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, cell, int(v)) } return err } // setCellTimeFunc provides a method to process time type of value for // SetCellValue. -func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { +func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, axis) + c, col, row, err := f.prepareCell(ws, cell) if err != nil { return err } ws.Lock() - cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) + c.S = f.prepareCellStyle(ws, col, row, c.S) ws.Unlock() date1904, wb := false, f.workbookReader() if wb != nil && wb.WorkbookPr != nil { date1904 = wb.WorkbookPr.Date1904 } var isNum bool - cellData.T, cellData.V, isNum, err = setCellTime(value, date1904) + c.T, c.V, isNum, err = setCellTime(value, date1904) if err != nil { return err } if isNum { - _ = f.setDefaultTimeStyle(sheet, axis, 22) + _ = f.setDefaultTimeStyle(sheet, cell, 22) } return err } @@ -270,22 +271,22 @@ func setCellDuration(value time.Duration) (t string, v string) { } // SetCellInt provides a function to set int type value of a cell by given -// worksheet name, cell coordinates and cell value. -func (f *File) SetCellInt(sheet, axis string, value int) error { +// worksheet name, cell reference and cell value. +func (f *File) SetCellInt(sheet, cell string, value int) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, axis) + c, col, row, err := f.prepareCell(ws, cell) if err != nil { return err } ws.Lock() defer ws.Unlock() - cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) - cellData.T, cellData.V = setCellInt(value) - cellData.IS = nil - f.removeFormula(cellData, ws, sheet) + c.S = f.prepareCellStyle(ws, col, row, c.S) + c.T, c.V = setCellInt(value) + c.IS = nil + f.removeFormula(c, ws, sheet) return err } @@ -297,22 +298,22 @@ func setCellInt(value int) (t string, v string) { } // SetCellBool provides a function to set bool type value of a cell by given -// worksheet name, cell name and cell value. -func (f *File) SetCellBool(sheet, axis string, value bool) error { +// worksheet name, cell reference and cell value. +func (f *File) SetCellBool(sheet, cell string, value bool) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, axis) + c, col, row, err := f.prepareCell(ws, cell) if err != nil { return err } ws.Lock() defer ws.Unlock() - cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) - cellData.T, cellData.V = setCellBool(value) - cellData.IS = nil - f.removeFormula(cellData, ws, sheet) + c.S = f.prepareCellStyle(ws, col, row, c.S) + c.T, c.V = setCellBool(value) + c.IS = nil + f.removeFormula(c, ws, sheet) return err } @@ -328,29 +329,29 @@ func setCellBool(value bool) (t string, v string) { return } -// SetCellFloat sets a floating point value into a cell. The precision parameter -// specifies how many places after the decimal will be shown while -1 is a -// special value that will use as many decimal places as necessary to -// represent the number. bitSize is 32 or 64 depending on if a float32 or -// float64 was originally used for the value. For Example: +// SetCellFloat sets a floating point value into a cell. The precision +// parameter specifies how many places after the decimal will be shown +// while -1 is a special value that will use as many decimal places as +// necessary to represent the number. bitSize is 32 or 64 depending on if a +// float32 or float64 was originally used for the value. For Example: // // var x float32 = 1.325 // f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) -func (f *File) SetCellFloat(sheet, axis string, value float64, precision, bitSize int) error { +func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSize int) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, axis) + c, col, row, err := f.prepareCell(ws, cell) if err != nil { return err } ws.Lock() defer ws.Unlock() - cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) - cellData.T, cellData.V = setCellFloat(value, precision, bitSize) - cellData.IS = nil - f.removeFormula(cellData, ws, sheet) + c.S = f.prepareCellStyle(ws, col, row, c.S) + c.T, c.V = setCellFloat(value, precision, bitSize) + c.IS = nil + f.removeFormula(c, ws, sheet) return err } @@ -363,21 +364,21 @@ func setCellFloat(value float64, precision, bitSize int) (t string, v string) { // SetCellStr provides a function to set string type value of a cell. Total // number of characters that a cell can contain 32767 characters. -func (f *File) SetCellStr(sheet, axis, value string) error { +func (f *File) SetCellStr(sheet, cell, value string) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, axis) + c, col, row, err := f.prepareCell(ws, cell) if err != nil { return err } ws.Lock() defer ws.Unlock() - cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) - cellData.T, cellData.V, err = f.setCellString(value) - cellData.IS = nil - f.removeFormula(cellData, ws, sheet) + c.S = f.prepareCellStyle(ws, col, row, c.S) + c.T, c.V, err = f.setCellString(value) + c.IS = nil + f.removeFormula(c, ws, sheet) return err } @@ -463,21 +464,21 @@ func setCellStr(value string) (t string, v string, ns xml.Attr) { // SetCellDefault provides a function to set string type value of a cell as // default format without escaping the cell. -func (f *File) SetCellDefault(sheet, axis, value string) error { +func (f *File) SetCellDefault(sheet, cell, value string) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, axis) + c, col, row, err := f.prepareCell(ws, cell) if err != nil { return err } ws.Lock() defer ws.Unlock() - cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) - cellData.T, cellData.V = setCellDefault(value) - cellData.IS = nil - f.removeFormula(cellData, ws, sheet) + c.S = f.prepareCellStyle(ws, col, row, c.S) + c.T, c.V = setCellDefault(value) + c.IS = nil + f.removeFormula(c, ws, sheet) return err } @@ -492,9 +493,9 @@ func setCellDefault(value string) (t string, v string) { } // GetCellFormula provides a function to get formula from cell by given -// worksheet name and axis in XLSX file. -func (f *File) GetCellFormula(sheet, axis string) (string, error) { - return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { +// worksheet name and cell reference in spreadsheet. +func (f *File) GetCellFormula(sheet, cell string) (string, error) { + return f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { if c.F == nil { return "", false, nil } @@ -587,44 +588,44 @@ type FormulaOpts struct { // fmt.Println(err) // } // } -func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) error { +func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, _, _, err := f.prepareCell(ws, axis) + c, _, _, err := f.prepareCell(ws, cell) if err != nil { return err } if formula == "" { - cellData.F = nil - f.deleteCalcChain(f.getSheetID(sheet), axis) + c.F = nil + f.deleteCalcChain(f.getSheetID(sheet), cell) return err } - if cellData.F != nil { - cellData.F.Content = formula + if c.F != nil { + c.F.Content = formula } else { - cellData.F = &xlsxF{Content: formula} + c.F = &xlsxF{Content: formula} } - for _, o := range opts { - if o.Type != nil { - if *o.Type == STCellFormulaTypeDataTable { + for _, opt := range opts { + if opt.Type != nil { + if *opt.Type == STCellFormulaTypeDataTable { return err } - cellData.F.T = *o.Type - if cellData.F.T == STCellFormulaTypeShared { - if err = ws.setSharedFormula(*o.Ref); err != nil { + c.F.T = *opt.Type + if c.F.T == STCellFormulaTypeShared { + if err = ws.setSharedFormula(*opt.Ref); err != nil { return err } } } - if o.Ref != nil { - cellData.F.Ref = *o.Ref + if opt.Ref != nil { + c.F.Ref = *opt.Ref } } - cellData.IS = nil + c.IS = nil return err } @@ -663,28 +664,28 @@ func (ws *xlsxWorksheet) countSharedFormula() (count int) { } // GetCellHyperLink gets a cell hyperlink based on the given worksheet name and -// cell coordinates. If the cell has a hyperlink, it will return 'true' and +// cell reference. If the cell has a hyperlink, it will return 'true' and // the link address, otherwise it will return 'false' and an empty link // address. // // For example, get a hyperlink to a 'H6' cell on a worksheet named 'Sheet1': // // link, target, err := f.GetCellHyperLink("Sheet1", "H6") -func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) { +func (f *File) GetCellHyperLink(sheet, cell string) (bool, string, error) { // Check for correct cell name - if _, _, err := SplitCellName(axis); err != nil { + if _, _, err := SplitCellName(cell); err != nil { return false, "", err } ws, err := f.workSheetReader(sheet) if err != nil { return false, "", err } - if axis, err = f.mergeCellsParser(ws, axis); err != nil { + if cell, err = f.mergeCellsParser(ws, cell); err != nil { return false, "", err } if ws.Hyperlinks != nil { for _, link := range ws.Hyperlinks.Hyperlink { - if link.Ref == axis { + if link.Ref == cell { if link.RID != "" { return true, f.getSheetRelationshipsTargetByID(sheet, link.RID), err } @@ -731,9 +732,9 @@ type HyperlinkOpts struct { // This is another example for "Location": // // err := f.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") -func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...HyperlinkOpts) error { +func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...HyperlinkOpts) error { // Check for correct cell name - if _, _, err := SplitCellName(axis); err != nil { + if _, _, err := SplitCellName(cell); err != nil { return err } @@ -741,7 +742,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...Hype if err != nil { return err } - if axis, err = f.mergeCellsParser(ws, axis); err != nil { + if cell, err = f.mergeCellsParser(ws, cell); err != nil { return err } @@ -751,7 +752,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...Hype ws.Hyperlinks = new(xlsxHyperlinks) } for i, hyperlink := range ws.Hyperlinks.Hyperlink { - if hyperlink.Ref == axis { + if hyperlink.Ref == cell { idx = i linkData = hyperlink break @@ -768,13 +769,13 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...Hype sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" rID := f.setRels(linkData.RID, sheetRels, SourceRelationshipHyperLink, link, linkType) linkData = xlsxHyperlink{ - Ref: axis, + Ref: cell, } linkData.RID = "rId" + strconv.Itoa(rID) f.addSheetNameSpace(sheet, SourceRelationship) case "Location": linkData = xlsxHyperlink{ - Ref: axis, + Ref: cell, Location: link, } default: @@ -837,12 +838,12 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro if err != nil { return } - cellData, _, _, err := f.prepareCell(ws, cell) + c, _, _, err := f.prepareCell(ws, cell) if err != nil { return } - siIdx, err := strconv.Atoi(cellData.V) - if err != nil || cellData.T != "s" { + siIdx, err := strconv.Atoi(c.V) + if err != nil || c.T != "s" { return } sst := f.sharedStringsReader() @@ -1007,14 +1008,14 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { if err != nil { return err } - cellData, col, row, err := f.prepareCell(ws, cell) + c, col, row, err := f.prepareCell(ws, cell) if err != nil { return err } if err := f.sharedStringsLoader(); err != nil { return err } - cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) + c.S = f.prepareCellStyle(ws, col, row, c.S) si := xlsxSI{} sst := f.sharedStringsReader() var textRuns []xlsxR @@ -1035,39 +1036,39 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { si.R = textRuns for idx, strItem := range sst.SI { if reflect.DeepEqual(strItem, si) { - cellData.T, cellData.V = "s", strconv.Itoa(idx) + c.T, c.V = "s", strconv.Itoa(idx) return err } } sst.SI = append(sst.SI, si) sst.Count++ sst.UniqueCount++ - cellData.T, cellData.V = "s", strconv.Itoa(len(sst.SI)-1) + c.T, c.V = "s", strconv.Itoa(len(sst.SI)-1) return err } // SetSheetRow writes an array to row by given worksheet name, starting -// coordinate and a pointer to array type 'slice'. This function is +// cell reference and a pointer to array type 'slice'. This function is // concurrency safe. For example, writes an array to row 6 start with the cell // B6 on Sheet1: // // err := f.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) -func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { - return f.setSheetCells(sheet, axis, slice, rows) +func (f *File) SetSheetRow(sheet, cell string, slice interface{}) error { + return f.setSheetCells(sheet, cell, slice, rows) } // SetSheetCol writes an array to column by given worksheet name, starting -// coordinate and a pointer to array type 'slice'. For example, writes an +// cell reference and a pointer to array type 'slice'. For example, writes an // array to column B start with the cell B6 on Sheet1: // // err := f.SetSheetCol("Sheet1", "B6", &[]interface{}{"1", nil, 2}) -func (f *File) SetSheetCol(sheet, axis string, slice interface{}) error { - return f.setSheetCells(sheet, axis, slice, columns) +func (f *File) SetSheetCol(sheet, cell string, slice interface{}) error { + return f.setSheetCells(sheet, cell, slice, columns) } // setSheetCells provides a function to set worksheet cells value. -func (f *File) setSheetCells(sheet, axis string, slice interface{}, dir adjustDirection) error { - col, row, err := CellNameToCoordinates(axis) +func (f *File) setSheetCells(sheet, cell string, slice interface{}, dir adjustDirection) error { + col, row, err := CellNameToCoordinates(cell) if err != nil { return err } @@ -1117,16 +1118,16 @@ func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, er // getCellStringFunc does common value extraction workflow for all GetCell* // methods. Passed function implements specific part of required logic. -func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool, error)) (string, error) { +func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool, error)) (string, error) { ws, err := f.workSheetReader(sheet) if err != nil { return "", err } - axis, err = f.mergeCellsParser(ws, axis) + cell, err = f.mergeCellsParser(ws, cell) if err != nil { return "", err } - _, row, err := CellNameToCoordinates(axis) + _, row, err := CellNameToCoordinates(cell) if err != nil { return "", err } @@ -1151,7 +1152,7 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c } for colIdx := range rowData.C { colData := &rowData.C[colIdx] - if axis != colData.R { + if cell != colData.R { continue } val, ok, err := fn(ws, colData) @@ -1224,9 +1225,9 @@ func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int { } // mergeCellsParser provides a function to check merged cells in worksheet by -// given axis. -func (f *File) mergeCellsParser(ws *xlsxWorksheet, axis string) (string, error) { - axis = strings.ToUpper(axis) +// given cell reference. +func (f *File) mergeCellsParser(ws *xlsxWorksheet, cell string) (string, error) { + cell = strings.ToUpper(cell) if ws.MergeCells != nil { for i := 0; i < len(ws.MergeCells.Cells); i++ { if ws.MergeCells.Cells[i] == nil { @@ -1234,20 +1235,20 @@ func (f *File) mergeCellsParser(ws *xlsxWorksheet, axis string) (string, error) i-- continue } - ok, err := f.checkCellInArea(axis, ws.MergeCells.Cells[i].Ref) + ok, err := f.checkCellInArea(cell, ws.MergeCells.Cells[i].Ref) if err != nil { - return axis, err + return cell, err } if ok { - axis = strings.Split(ws.MergeCells.Cells[i].Ref, ":")[0] + cell = strings.Split(ws.MergeCells.Cells[i].Ref, ":")[0] } } } - return axis, nil + return cell, nil } -// checkCellInArea provides a function to determine if a given coordinate is -// within an area. +// checkCellInArea provides a function to determine if a given cell reference +// in a range. func (f *File) checkCellInArea(cell, area string) (bool, error) { col, row, err := CellNameToCoordinates(cell) if err != nil { @@ -1333,11 +1334,11 @@ func parseSharedFormula(dCol, dRow int, orig []byte) (res string, start int) { // // Note that this function not validate ref tag to check the cell whether in // allow area, and always return origin shared formula. -func getSharedFormula(ws *xlsxWorksheet, si int, axis string) string { +func getSharedFormula(ws *xlsxWorksheet, si int, cell string) string { for _, r := range ws.SheetData.Row { for _, c := range r.C { if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si != nil && *c.F.Si == si { - col, row, _ := CellNameToCoordinates(axis) + col, row, _ := CellNameToCoordinates(cell) sharedCol, sharedRow, _ := CellNameToCoordinates(c.R) dCol := col - sharedCol dRow := row - sharedRow diff --git a/cell_test.go b/cell_test.go index 5b8e639a79..9c8b511d75 100644 --- a/cell_test.go +++ b/cell_test.go @@ -573,7 +573,7 @@ func TestGetCellRichText(t *testing.T) { // Test set cell rich text on not exists worksheet _, err = f.GetCellRichText("SheetN", "A1") assert.EqualError(t, err, "sheet SheetN does not exist") - // Test set cell rich text with illegal cell coordinates + // Test set cell rich text with illegal cell reference _, err = f.GetCellRichText("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } @@ -670,7 +670,7 @@ func TestSetCellRichText(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellRichText.xlsx"))) // Test set cell rich text on not exists worksheet assert.EqualError(t, f.SetCellRichText("SheetN", "A1", richTextRun), "sheet SheetN does not exist") - // Test set cell rich text with illegal cell coordinates + // Test set cell rich text with illegal cell reference assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) richTextRun = []RichTextRun{{Text: strings.Repeat("s", TotalCellChars+1)}} // Test set cell rich text with characters over the maximum limit diff --git a/chart.go b/chart.go index 5f7ae3daad..267e0ddc1e 100644 --- a/chart.go +++ b/chart.go @@ -984,8 +984,8 @@ func (f *File) getFormatChart(format string, combo []string) (*formatChart, []*f return formatSet, comboCharts, err } -// DeleteChart provides a function to delete chart in XLSX by given worksheet -// and cell name. +// DeleteChart provides a function to delete chart in spreadsheet by given +// worksheet name and cell reference. func (f *File) DeleteChart(sheet, cell string) (err error) { col, row, err := CellNameToCoordinates(cell) if err != nil { diff --git a/chart_test.go b/chart_test.go index 9184f26e36..bd633761ab 100644 --- a/chart_test.go +++ b/chart_test.go @@ -198,7 +198,7 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"areaStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[1]), fmt.Sprintf(`{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[0]))) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) - // Test with illegal cell coordinates + // Test with illegal cell reference assert.EqualError(t, f.AddChart("Sheet2", "A", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // Test with unsupported chart type assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"unknown","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bubble 3D Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`), "unsupported chart type unknown") diff --git a/col_test.go b/col_test.go index f01ffdc509..75c191b93a 100644 --- a/col_test.go +++ b/col_test.go @@ -207,7 +207,7 @@ func TestColumnVisibility(t *testing.T) { _, err = f.GetColVisible("SheetN", "F") assert.EqualError(t, err, "sheet SheetN does not exist") - // Test get column visible with illegal cell coordinates. + // Test get column visible with illegal cell reference. _, err = f.GetColVisible("Sheet1", "*") assert.EqualError(t, err, newInvalidColumnNameError("*").Error()) assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), newInvalidColumnNameError("*").Error()) @@ -258,7 +258,7 @@ func TestOutlineLevel(t *testing.T) { _, err = f.GetRowOutlineLevel("SheetN", 1) assert.EqualError(t, err, "sheet SheetN does not exist") - // Test set and get column outline level with illegal cell coordinates. + // Test set and get column outline level with illegal cell reference. assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), newInvalidColumnNameError("*").Error()) _, err = f.GetColOutlineLevel("Sheet1", "*") assert.EqualError(t, err, newInvalidColumnNameError("*").Error()) @@ -329,7 +329,7 @@ func TestColWidth(t *testing.T) { assert.Equal(t, defaultColWidth, width) assert.NoError(t, err) - // Test set and get column width with illegal cell coordinates. + // Test set and get column width with illegal cell reference. width, err = f.GetColWidth("Sheet1", "*") assert.Equal(t, defaultColWidth, width) assert.EqualError(t, err, newInvalidColumnNameError("*").Error()) @@ -373,7 +373,7 @@ func TestInsertCols(t *testing.T) { assert.NoError(t, f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`)) assert.NoError(t, f.InsertCols(sheet1, "A", 1)) - // Test insert column with illegal cell coordinates. + // Test insert column with illegal cell reference. assert.EqualError(t, f.InsertCols(sheet1, "*", 1), newInvalidColumnNameError("*").Error()) assert.EqualError(t, f.InsertCols(sheet1, "A", 0), ErrColumnNumber.Error()) @@ -398,7 +398,7 @@ func TestRemoveCol(t *testing.T) { assert.NoError(t, f.RemoveCol(sheet1, "A")) assert.NoError(t, f.RemoveCol(sheet1, "A")) - // Test remove column with illegal cell coordinates. + // Test remove column with illegal cell reference. assert.EqualError(t, f.RemoveCol("Sheet1", "*"), newInvalidColumnNameError("*").Error()) // Test remove column on not exists worksheet. diff --git a/comment.go b/comment.go index a75ea7f4cb..41f91bb2bf 100644 --- a/comment.go +++ b/comment.go @@ -141,7 +141,7 @@ func (f *File) AddComment(sheet, cell, format string) error { } // DeleteComment provides the method to delete comment in a sheet by given -// worksheet. For example, delete the comment in Sheet1!$A$30: +// worksheet name. For example, delete the comment in Sheet1!$A$30: // // err := f.DeleteComment("Sheet1", "A30") func (f *File) DeleteComment(sheet, cell string) (err error) { diff --git a/comment_test.go b/comment_test.go index 0d1e039e4b..2beca70c2e 100644 --- a/comment_test.go +++ b/comment_test.go @@ -32,7 +32,7 @@ func TestAddComments(t *testing.T) { // Test add comment on not exists worksheet. assert.EqualError(t, f.AddComment("SheetN", "B7", `{"author":"Excelize: ","text":"This is a comment."}`), "sheet SheetN does not exist") - // Test add comment on with illegal cell coordinates + // Test add comment on with illegal cell reference assert.EqualError(t, f.AddComment("Sheet1", "A", `{"author":"Excelize: ","text":"This is a comment."}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { assert.Len(t, f.GetComments(), 2) diff --git a/crypt.go b/crypt.go index 58a1c99bf6..5dd8b0c122 100644 --- a/crypt.go +++ b/crypt.go @@ -139,7 +139,7 @@ type encryption struct { // Decrypt API decrypts the CFB file format with ECMA-376 agile encryption and // standard encryption. Support cryptographic algorithm: MD4, MD5, RIPEMD-160, // SHA1, SHA256, SHA384 and SHA512 currently. -func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { +func Decrypt(raw []byte, opts *Options) (packageBuf []byte, err error) { doc, err := mscfb.New(bytes.NewReader(raw)) if err != nil { return @@ -150,13 +150,13 @@ func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { return } if mechanism == "agile" { - return agileDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt) + return agileDecrypt(encryptionInfoBuf, encryptedPackageBuf, opts) } - return standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt) + return standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, opts) } // Encrypt API encrypt data with the password. -func Encrypt(raw []byte, opt *Options) ([]byte, error) { +func Encrypt(raw []byte, opts *Options) ([]byte, error) { encryptor := encryption{ EncryptedVerifierHashInput: make([]byte, 16), EncryptedVerifierHashValue: make([]byte, 32), @@ -166,7 +166,7 @@ func Encrypt(raw []byte, opt *Options) ([]byte, error) { SaltSize: 16, } // Key Encryption - encryptionInfoBuffer, err := encryptor.standardKeyEncryption(opt.Password) + encryptionInfoBuffer, err := encryptor.standardKeyEncryption(opts.Password) if err != nil { return nil, err } @@ -228,7 +228,7 @@ func encryptionMechanism(buffer []byte) (mechanism string, err error) { // ECMA-376 Standard Encryption // standardDecrypt decrypt the CFB file format with ECMA-376 standard encryption. -func standardDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options) ([]byte, error) { +func standardDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opts *Options) ([]byte, error) { encryptionHeaderSize := binary.LittleEndian.Uint32(encryptionInfoBuf[8:12]) block := encryptionInfoBuf[12 : 12+encryptionHeaderSize] header := StandardEncryptionHeader{ @@ -254,7 +254,7 @@ func standardDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options algorithm = "RC4" } verifier := standardEncryptionVerifier(algorithm, block) - secretKey, err := standardConvertPasswdToKey(header, verifier, opt) + secretKey, err := standardConvertPasswdToKey(header, verifier, opts) if err != nil { return nil, err } @@ -289,9 +289,9 @@ func standardEncryptionVerifier(algorithm string, blob []byte) StandardEncryptio } // standardConvertPasswdToKey generate intermediate key from given password. -func standardConvertPasswdToKey(header StandardEncryptionHeader, verifier StandardEncryptionVerifier, opt *Options) ([]byte, error) { +func standardConvertPasswdToKey(header StandardEncryptionHeader, verifier StandardEncryptionVerifier, opts *Options) ([]byte, error) { encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() - passwordBuffer, err := encoder.Bytes([]byte(opt.Password)) + passwordBuffer, err := encoder.Bytes([]byte(opts.Password)) if err != nil { return nil, err } @@ -395,13 +395,13 @@ func (e *encryption) standardKeyEncryption(password string) ([]byte, error) { // agileDecrypt decrypt the CFB file format with ECMA-376 agile encryption. // Support cryptographic algorithm: MD4, MD5, RIPEMD-160, SHA1, SHA256, // SHA384 and SHA512. -func agileDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options) (packageBuf []byte, err error) { +func agileDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opts *Options) (packageBuf []byte, err error) { var encryptionInfo Encryption if encryptionInfo, err = parseEncryptionInfo(encryptionInfoBuf[8:]); err != nil { return } // Convert the password into an encryption key. - key, err := convertPasswdToKey(opt.Password, blockKey, encryptionInfo) + key, err := convertPasswdToKey(opts.Password, blockKey, encryptionInfo) if err != nil { return } diff --git a/excelize.go b/excelize.go index bb4bde063a..fd6a463a97 100644 --- a/excelize.go +++ b/excelize.go @@ -98,12 +98,12 @@ type Options struct { // } // // Close the file by Close function after opening the spreadsheet. -func OpenFile(filename string, opt ...Options) (*File, error) { +func OpenFile(filename string, opts ...Options) (*File, error) { file, err := os.Open(filepath.Clean(filename)) if err != nil { return nil, err } - f, err := OpenReader(file, opt...) + f, err := OpenReader(file, opts...) if err != nil { closeErr := file.Close() if closeErr == nil { @@ -188,11 +188,11 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) { // parseOptions provides a function to parse the optional settings for open // and reading spreadsheet. func parseOptions(opts ...Options) *Options { - opt := &Options{} - for _, o := range opts { - opt = &o + options := &Options{} + for _, opt := range opts { + options = &opt } - return opt + return options } // CharsetTranscoder Set user defined codepage transcoder function for open @@ -207,16 +207,16 @@ func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) { } // setDefaultTimeStyle provides a function to set default numbers format for -// time.Time type cell value by given worksheet name, cell coordinates and +// time.Time type cell value by given worksheet name, cell reference and // number format code. -func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error { - s, err := f.GetCellStyle(sheet, axis) +func (f *File) setDefaultTimeStyle(sheet, cell string, format int) error { + s, err := f.GetCellStyle(sheet, cell) if err != nil { return err } if s == 0 { style, _ := f.NewStyle(&Style{NumFmt: format}) - err = f.SetCellStyle(sheet, axis, axis, style) + err = f.SetCellStyle(sheet, cell, cell, style) } return err } diff --git a/excelize_test.go b/excelize_test.go index 9d60b1c30e..5756e6eafc 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -165,7 +165,7 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet2", "G5", time.Duration(1e13))) // Test completion column. assert.NoError(t, f.SetCellValue("Sheet2", "M2", nil)) - // Test read cell value with given axis large than exists row. + // Test read cell value with given cell reference large than exists row. _, err = f.GetCellValue("Sheet2", "E231") assert.NoError(t, err) // Test get active worksheet of spreadsheet and get worksheet name of spreadsheet by given worksheet index. @@ -336,7 +336,7 @@ func TestNewFile(t *testing.T) { } func TestAddDrawingVML(t *testing.T) { - // Test addDrawingVML with illegal cell coordinates. + // Test addDrawingVML with illegal cell reference. f := NewFile() assert.EqualError(t, f.addDrawingVML(0, "", "*", 0, 0), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error()) } diff --git a/lib.go b/lib.go index 0408139c64..5feb7d5951 100644 --- a/lib.go +++ b/lib.go @@ -261,7 +261,7 @@ func CellNameToCoordinates(cell string) (int, int, error) { // excelize.CoordinatesToCellName(1, 1, true) // returns "$A$1", nil func CoordinatesToCellName(col, row int, abs ...bool) (string, error) { if col < 1 || row < 1 { - return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row) + return "", fmt.Errorf("invalid cell reference [%d, %d]", col, row) } sign := "" for _, a := range abs { @@ -273,7 +273,7 @@ func CoordinatesToCellName(col, row int, abs ...bool) (string, error) { return sign + colName + sign + strconv.Itoa(row), err } -// areaRefToCoordinates provides a function to convert area reference to a +// areaRefToCoordinates provides a function to convert range reference to a // pair of coordinates. func areaRefToCoordinates(ref string) ([]int, error) { rng := strings.Split(strings.ReplaceAll(ref, "$", ""), ":") @@ -296,7 +296,7 @@ func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) { return coordinates, err } -// sortCoordinates provides a function to correct the coordinate area, such +// sortCoordinates provides a function to correct the cell range, such // correct C1:B3 to B1:C3. func sortCoordinates(coordinates []int) error { if len(coordinates) != 4 { @@ -349,7 +349,7 @@ func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string) return } -// flatSqref convert reference sequence to cell coordinates list. +// flatSqref convert reference sequence to cell reference list. func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) { var coordinates []int cells = make(map[int][][]int) @@ -524,14 +524,14 @@ func namespaceStrictToTransitional(content []byte) []byte { return content } -// bytesReplace replace old bytes with given new. -func bytesReplace(s, old, new []byte, n int) []byte { +// bytesReplace replace source bytes with given target. +func bytesReplace(s, source, target []byte, n int) []byte { if n == 0 { return s } - if len(old) < len(new) { - return bytes.Replace(s, old, new, n) + if len(source) < len(target) { + return bytes.Replace(s, source, target, n) } if n < 0 { @@ -540,14 +540,14 @@ func bytesReplace(s, old, new []byte, n int) []byte { var wid, i, j, w int for i, j = 0, 0; i < len(s) && j < n; j++ { - wid = bytes.Index(s[i:], old) + wid = bytes.Index(s[i:], source) if wid < 0 { break } w += copy(s[w:], s[i:i+wid]) - w += copy(s[w:], new) - i += wid + len(old) + w += copy(s[w:], target) + i += wid + len(source) } w += copy(s[w:], s[i:]) diff --git a/lib_test.go b/lib_test.go index 5fa644eaf0..c42914d8aa 100644 --- a/lib_test.go +++ b/lib_test.go @@ -222,9 +222,9 @@ func TestCoordinatesToAreaRef(t *testing.T) { _, err := f.coordinatesToAreaRef([]int{}) assert.EqualError(t, err, ErrCoordinates.Error()) _, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1}) - assert.EqualError(t, err, "invalid cell coordinates [1, -1]") + assert.EqualError(t, err, "invalid cell reference [1, -1]") _, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1}) - assert.EqualError(t, err, "invalid cell coordinates [1, -1]") + assert.EqualError(t, err, "invalid cell reference [1, -1]") ref, err := f.coordinatesToAreaRef([]int{1, 1, 1, 1}) assert.NoError(t, err) assert.EqualValues(t, ref, "A1:A1") diff --git a/merge.go b/merge.go index c31416a262..ac7fb0478e 100644 --- a/merge.go +++ b/merge.go @@ -22,7 +22,7 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) { return mc.rect, err } -// MergeCell provides a function to merge cells by given coordinate area and +// MergeCell provides a function to merge cells by given range reference and // sheet name. Merging cells only keeps the upper-left cell value, and // discards the other values. For example create a merged cell of D3:E9 on // Sheet1: @@ -30,7 +30,7 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) { // err := f.MergeCell("Sheet1", "D3", "E9") // // If you create a merged cell that overlaps with another existing merged cell, -// those merged cells that already exist will be removed. The cell coordinates +// those merged cells that already exist will be removed. The cell references // tuple after merging in the following range will be: A1(x3,y1) D1(x2,y1) // A8(x3,y4) D8(x2,y4) // @@ -50,7 +50,7 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error { if err != nil { return err } - // Correct the coordinate area, such correct C1:B3 to B1:C3. + // Correct the range reference, such correct C1:B3 to B1:C3. _ = sortCoordinates(rect) hCell, _ = CoordinatesToCellName(rect[0], rect[1]) @@ -72,13 +72,13 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error { return err } -// UnmergeCell provides a function to unmerge a given coordinate area. +// UnmergeCell provides a function to unmerge a given range reference. // For example unmerge area D3:E9 on Sheet1: // // err := f.UnmergeCell("Sheet1", "D3", "E9") // // Attention: overlapped areas will also be unmerged. -func (f *File) UnmergeCell(sheet string, hCell, vCell string) error { +func (f *File) UnmergeCell(sheet, hCell, vCell string) error { ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -90,7 +90,7 @@ func (f *File) UnmergeCell(sheet string, hCell, vCell string) error { return err } - // Correct the coordinate area, such correct C1:B3 to B1:C3. + // Correct the range reference, such correct C1:B3 to B1:C3. _ = sortCoordinates(rect1) // return nil since no MergeCells in the sheet @@ -135,8 +135,8 @@ func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { mergeCells = make([]MergeCell, 0, len(ws.MergeCells.Cells)) for i := range ws.MergeCells.Cells { ref := ws.MergeCells.Cells[i].Ref - axis := strings.Split(ref, ":")[0] - val, _ := f.GetCellValue(sheet, axis) + cell := strings.Split(ref, ":")[0] + val, _ := f.GetCellValue(sheet, cell) mergeCells = append(mergeCells, []string{ref, val}) } } @@ -272,16 +272,14 @@ func (m *MergeCell) GetCellValue() string { return (*m)[1] } -// GetStartAxis returns the top left cell coordinates of merged range, for +// GetStartAxis returns the top left cell reference of merged range, for // example: "C2". func (m *MergeCell) GetStartAxis() string { - axis := strings.Split((*m)[0], ":") - return axis[0] + return strings.Split((*m)[0], ":")[0] } -// GetEndAxis returns the bottom right cell coordinates of merged range, for +// GetEndAxis returns the bottom right cell reference of merged range, for // example: "D4". func (m *MergeCell) GetEndAxis() string { - axis := strings.Split((*m)[0], ":") - return axis[1] + return strings.Split((*m)[0], ":")[1] } diff --git a/picture.go b/picture.go index 30a255d4d1..3b6d821380 100644 --- a/picture.go +++ b/picture.go @@ -512,8 +512,8 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { } // DeletePicture provides a function to delete charts in spreadsheet by given -// worksheet and cell name. Note that the image file won't be deleted from the -// document currently. +// worksheet name and cell reference. Note that the image file won't be deleted +// from the document currently. func (f *File) DeletePicture(sheet, cell string) (err error) { col, row, err := CellNameToCoordinates(cell) if err != nil { diff --git a/picture_test.go b/picture_test.go index 3588218627..d419378ba5 100644 --- a/picture_test.go +++ b/picture_test.go @@ -57,7 +57,7 @@ func TestAddPicture(t *testing.T) { // Test add picture to worksheet from bytes. assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file)) - // Test add picture to worksheet from bytes with illegal cell coordinates. + // Test add picture to worksheet from bytes with illegal cell reference. assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", "", "Excel Logo", ".png", file), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.AddPicture("Sheet1", "Q8", filepath.Join("test", "images", "excel.gif"), "")) @@ -118,7 +118,7 @@ func TestGetPicture(t *testing.T) { t.FailNow() } - // Try to get picture from a worksheet with illegal cell coordinates. + // Try to get picture from a worksheet with illegal cell reference. _, _, err = f.GetPicture("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) @@ -173,7 +173,7 @@ func TestGetPicture(t *testing.T) { } func TestAddDrawingPicture(t *testing.T) { - // Test addDrawingPicture with illegal cell coordinates. + // Test addDrawingPicture with illegal cell reference. f := NewFile() assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, 0, 0, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } diff --git a/pivotTable.go b/pivotTable.go index 1ef0333501..af30a0b722 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -27,26 +27,26 @@ import ( // PivotStyleDark1 - PivotStyleDark28 type PivotTableOption struct { pivotTableSheetName string - DataRange string - PivotTableRange string - Rows []PivotTableField - Columns []PivotTableField - Data []PivotTableField - Filter []PivotTableField - RowGrandTotals bool - ColGrandTotals bool - ShowDrill bool - UseAutoFormatting bool - PageOverThenDown bool - MergeItem bool - CompactData bool - ShowError bool - ShowRowHeaders bool - ShowColHeaders bool - ShowRowStripes bool - ShowColStripes bool - ShowLastColumn bool - PivotTableStyleName string + DataRange string `json:"data_range"` + PivotTableRange string `json:"pivot_table_range"` + Rows []PivotTableField `json:"rows"` + Columns []PivotTableField `json:"columns"` + Data []PivotTableField `json:"data"` + Filter []PivotTableField `json:"filter"` + RowGrandTotals bool `json:"row_grand_totals"` + ColGrandTotals bool `json:"col_grand_totals"` + ShowDrill bool `json:"show_drill"` + UseAutoFormatting bool `json:"use_auto_formatting"` + PageOverThenDown bool `json:"page_over_then_down"` + MergeItem bool `json:"merge_item"` + CompactData bool `json:"compact_data"` + ShowError bool `json:"show_error"` + ShowRowHeaders bool `json:"show_row_headers"` + ShowColHeaders bool `json:"show_col_headers"` + ShowRowStripes bool `json:"show_row_stripes"` + ShowColStripes bool `json:"show_col_stripes"` + ShowLastColumn bool `json:"show_last_column"` + PivotTableStyleName string `json:"pivot_table_style_name"` } // PivotTableField directly maps the field settings of the pivot table. @@ -69,12 +69,12 @@ type PivotTableOption struct { // Name specifies the name of the data field. Maximum 255 characters // are allowed in data field name, excess characters will be truncated. type PivotTableField struct { - Compact bool - Data string - Name string - Outline bool - Subtotal string - DefaultSubtotal bool + Compact bool `json:"compact"` + Data string `json:"data"` + Name string `json:"name"` + Outline bool `json:"outline"` + Subtotal string `json:"subtotal"` + DefaultSubtotal bool `json:"default_subtotal"` } // AddPivotTable provides the method to add pivot table by given pivot table @@ -128,9 +128,9 @@ type PivotTableField struct { // fmt.Println(err) // } // } -func (f *File) AddPivotTable(opt *PivotTableOption) error { +func (f *File) AddPivotTable(opts *PivotTableOption) error { // parameter validation - _, pivotTableSheetPath, err := f.parseFormatPivotTableSet(opt) + _, pivotTableSheetPath, err := f.parseFormatPivotTableSet(opts) if err != nil { return err } @@ -141,7 +141,7 @@ func (f *File) AddPivotTable(opt *PivotTableOption) error { sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml" pivotTableXML := strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl") pivotCacheXML := "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml" - err = f.addPivotCache(pivotCacheXML, opt) + err = f.addPivotCache(pivotCacheXML, opts) if err != nil { return err } @@ -153,7 +153,7 @@ func (f *File) AddPivotTable(opt *PivotTableOption) error { pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels" // rId not used _ = f.addRels(pivotCacheRels, SourceRelationshipPivotCache, fmt.Sprintf("../pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") - err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opt) + err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opts) if err != nil { return err } @@ -167,18 +167,18 @@ func (f *File) AddPivotTable(opt *PivotTableOption) error { // parseFormatPivotTableSet provides a function to validate pivot table // properties. -func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet, string, error) { - if opt == nil { +func (f *File) parseFormatPivotTableSet(opts *PivotTableOption) (*xlsxWorksheet, string, error) { + if opts == nil { return nil, "", ErrParameterRequired } - pivotTableSheetName, _, err := f.adjustRange(opt.PivotTableRange) + pivotTableSheetName, _, err := f.adjustRange(opts.PivotTableRange) if err != nil { return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) } - opt.pivotTableSheetName = pivotTableSheetName - dataRange := f.getDefinedNameRefTo(opt.DataRange, pivotTableSheetName) + opts.pivotTableSheetName = pivotTableSheetName + dataRange := f.getDefinedNameRefTo(opts.DataRange, pivotTableSheetName) if dataRange == "" { - dataRange = opt.DataRange + dataRange = opts.DataRange } dataSheetName, _, err := f.adjustRange(dataRange) if err != nil { @@ -214,7 +214,7 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { return rng[0], []int{}, ErrParameterInvalid } - // Correct the coordinate area, such correct C1:B3 to B1:C3. + // Correct the range, such correct C1:B3 to B1:C3. if x2 < x1 { x1, x2 = x2, x1 } @@ -227,11 +227,11 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { // getPivotFieldsOrder provides a function to get order list of pivot table // fields. -func (f *File) getPivotFieldsOrder(opt *PivotTableOption) ([]string, error) { +func (f *File) getPivotFieldsOrder(opts *PivotTableOption) ([]string, error) { var order []string - dataRange := f.getDefinedNameRefTo(opt.DataRange, opt.pivotTableSheetName) + dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName) if dataRange == "" { - dataRange = opt.DataRange + dataRange = opts.DataRange } dataSheet, coordinates, err := f.adjustRange(dataRange) if err != nil { @@ -249,20 +249,20 @@ func (f *File) getPivotFieldsOrder(opt *PivotTableOption) ([]string, error) { } // addPivotCache provides a function to create a pivot cache by given properties. -func (f *File) addPivotCache(pivotCacheXML string, opt *PivotTableOption) error { +func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOption) error { // validate data range definedNameRef := true - dataRange := f.getDefinedNameRefTo(opt.DataRange, opt.pivotTableSheetName) + dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName) if dataRange == "" { definedNameRef = false - dataRange = opt.DataRange + dataRange = opts.DataRange } dataSheet, coordinates, err := f.adjustRange(dataRange) if err != nil { return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) } // data range has been checked - order, _ := f.getPivotFieldsOrder(opt) + order, _ := f.getPivotFieldsOrder(opts) hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pc := xlsxPivotCacheDefinition{ @@ -281,11 +281,11 @@ func (f *File) addPivotCache(pivotCacheXML string, opt *PivotTableOption) error CacheFields: &xlsxCacheFields{}, } if definedNameRef { - pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opt.DataRange} + pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opts.DataRange} } for _, name := range order { - rowOptions, rowOk := f.getPivotTableFieldOptions(name, opt.Rows) - columnOptions, colOk := f.getPivotTableFieldOptions(name, opt.Columns) + rowOptions, rowOk := f.getPivotTableFieldOptions(name, opts.Rows) + columnOptions, colOk := f.getPivotTableFieldOptions(name, opts.Columns) sharedItems := xlsxSharedItems{ Count: 0, } @@ -311,9 +311,9 @@ func (f *File) addPivotCache(pivotCacheXML string, opt *PivotTableOption) error // addPivotTable provides a function to create a pivot table by given pivot // table ID and properties. -func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, opt *PivotTableOption) error { +func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, opts *PivotTableOption) error { // validate pivot table range - _, coordinates, err := f.adjustRange(opt.PivotTableRange) + _, coordinates, err := f.adjustRange(opts.PivotTableRange) if err != nil { return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) } @@ -322,25 +322,25 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pivotTableStyle := func() string { - if opt.PivotTableStyleName == "" { + if opts.PivotTableStyleName == "" { return "PivotStyleLight16" } - return opt.PivotTableStyleName + return opts.PivotTableStyleName } pt := xlsxPivotTableDefinition{ Name: fmt.Sprintf("Pivot Table%d", pivotTableID), CacheID: cacheID, - RowGrandTotals: &opt.RowGrandTotals, - ColGrandTotals: &opt.ColGrandTotals, + RowGrandTotals: &opts.RowGrandTotals, + ColGrandTotals: &opts.ColGrandTotals, UpdatedVersion: pivotTableVersion, MinRefreshableVersion: pivotTableVersion, - ShowDrill: &opt.ShowDrill, - UseAutoFormatting: &opt.UseAutoFormatting, - PageOverThenDown: &opt.PageOverThenDown, - MergeItem: &opt.MergeItem, + ShowDrill: &opts.ShowDrill, + UseAutoFormatting: &opts.UseAutoFormatting, + PageOverThenDown: &opts.PageOverThenDown, + MergeItem: &opts.MergeItem, CreatedVersion: pivotTableVersion, - CompactData: &opt.CompactData, - ShowError: &opt.ShowError, + CompactData: &opts.CompactData, + ShowError: &opts.ShowError, DataCaption: "Values", Location: &xlsxLocation{ Ref: hCell + ":" + vCell, @@ -363,25 +363,25 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op }, PivotTableStyleInfo: &xlsxPivotTableStyleInfo{ Name: pivotTableStyle(), - ShowRowHeaders: opt.ShowRowHeaders, - ShowColHeaders: opt.ShowColHeaders, - ShowRowStripes: opt.ShowRowStripes, - ShowColStripes: opt.ShowColStripes, - ShowLastColumn: opt.ShowLastColumn, + ShowRowHeaders: opts.ShowRowHeaders, + ShowColHeaders: opts.ShowColHeaders, + ShowRowStripes: opts.ShowRowStripes, + ShowColStripes: opts.ShowColStripes, + ShowLastColumn: opts.ShowLastColumn, }, } // pivot fields - _ = f.addPivotFields(&pt, opt) + _ = f.addPivotFields(&pt, opts) // count pivot fields pt.PivotFields.Count = len(pt.PivotFields.PivotField) // data range has been checked - _ = f.addPivotRowFields(&pt, opt) - _ = f.addPivotColFields(&pt, opt) - _ = f.addPivotPageFields(&pt, opt) - _ = f.addPivotDataFields(&pt, opt) + _ = f.addPivotRowFields(&pt, opts) + _ = f.addPivotColFields(&pt, opts) + _ = f.addPivotPageFields(&pt, opts) + _ = f.addPivotDataFields(&pt, opts) pivotTable, err := xml.Marshal(pt) f.saveFileList(pivotTableXML, pivotTable) @@ -390,9 +390,9 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op // addPivotRowFields provides a method to add row fields for pivot table by // given pivot table options. -func (f *File) addPivotRowFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { +func (f *File) addPivotRowFields(pt *xlsxPivotTableDefinition, opts *PivotTableOption) error { // row fields - rowFieldsIndex, err := f.getPivotFieldsIndex(opt.Rows, opt) + rowFieldsIndex, err := f.getPivotFieldsIndex(opts.Rows, opts) if err != nil { return err } @@ -414,13 +414,13 @@ func (f *File) addPivotRowFields(pt *xlsxPivotTableDefinition, opt *PivotTableOp // addPivotPageFields provides a method to add page fields for pivot table by // given pivot table options. -func (f *File) addPivotPageFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { +func (f *File) addPivotPageFields(pt *xlsxPivotTableDefinition, opts *PivotTableOption) error { // page fields - pageFieldsIndex, err := f.getPivotFieldsIndex(opt.Filter, opt) + pageFieldsIndex, err := f.getPivotFieldsIndex(opts.Filter, opts) if err != nil { return err } - pageFieldsName := f.getPivotTableFieldsName(opt.Filter) + pageFieldsName := f.getPivotTableFieldsName(opts.Filter) for idx, pageField := range pageFieldsIndex { if pt.PageFields == nil { pt.PageFields = &xlsxPageFields{} @@ -440,14 +440,14 @@ func (f *File) addPivotPageFields(pt *xlsxPivotTableDefinition, opt *PivotTableO // addPivotDataFields provides a method to add data fields for pivot table by // given pivot table options. -func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { +func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTableOption) error { // data fields - dataFieldsIndex, err := f.getPivotFieldsIndex(opt.Data, opt) + dataFieldsIndex, err := f.getPivotFieldsIndex(opts.Data, opts) if err != nil { return err } - dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opt.Data) - dataFieldsName := f.getPivotTableFieldsName(opt.Data) + dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opts.Data) + dataFieldsName := f.getPivotTableFieldsName(opts.Data) for idx, dataField := range dataFieldsIndex { if pt.DataFields == nil { pt.DataFields = &xlsxDataFields{} @@ -480,9 +480,9 @@ func inPivotTableField(a []PivotTableField, x string) int { // addPivotColFields create pivot column fields by given pivot table // definition and option. -func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { - if len(opt.Columns) == 0 { - if len(opt.Data) <= 1 { +func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opts *PivotTableOption) error { + if len(opts.Columns) == 0 { + if len(opts.Data) <= 1 { return nil } pt.ColFields = &xlsxColFields{} @@ -497,7 +497,7 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOp pt.ColFields = &xlsxColFields{} // col fields - colFieldsIndex, err := f.getPivotFieldsIndex(opt.Columns, opt) + colFieldsIndex, err := f.getPivotFieldsIndex(opts.Columns, opts) if err != nil { return err } @@ -508,7 +508,7 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOp } // in order to create pivot in case there is many Columns and Data - if len(opt.Data) > 1 { + if len(opts.Data) > 1 { pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ X: -2, }) @@ -521,15 +521,15 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOp // addPivotFields create pivot fields based on the column order of the first // row in the data region by given pivot table definition and option. -func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { - order, err := f.getPivotFieldsOrder(opt) +func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOption) error { + order, err := f.getPivotFieldsOrder(opts) if err != nil { return err } x := 0 for _, name := range order { - if inPivotTableField(opt.Rows, name) != -1 { - rowOptions, ok := f.getPivotTableFieldOptions(name, opt.Rows) + if inPivotTableField(opts.Rows, name) != -1 { + rowOptions, ok := f.getPivotTableFieldOptions(name, opts.Rows) var items []*xlsxItem if !ok || !rowOptions.DefaultSubtotal { items = append(items, &xlsxItem{X: &x}) @@ -538,9 +538,9 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio } pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ - Name: f.getPivotTableFieldName(name, opt.Rows), + Name: f.getPivotTableFieldName(name, opts.Rows), Axis: "axisRow", - DataField: inPivotTableField(opt.Data, name) != -1, + DataField: inPivotTableField(opts.Data, name) != -1, Compact: &rowOptions.Compact, Outline: &rowOptions.Outline, DefaultSubtotal: &rowOptions.DefaultSubtotal, @@ -551,11 +551,11 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio }) continue } - if inPivotTableField(opt.Filter, name) != -1 { + if inPivotTableField(opts.Filter, name) != -1 { pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ Axis: "axisPage", - DataField: inPivotTableField(opt.Data, name) != -1, - Name: f.getPivotTableFieldName(name, opt.Columns), + DataField: inPivotTableField(opts.Data, name) != -1, + Name: f.getPivotTableFieldName(name, opts.Columns), Items: &xlsxItems{ Count: 1, Item: []*xlsxItem{ @@ -565,8 +565,8 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio }) continue } - if inPivotTableField(opt.Columns, name) != -1 { - columnOptions, ok := f.getPivotTableFieldOptions(name, opt.Columns) + if inPivotTableField(opts.Columns, name) != -1 { + columnOptions, ok := f.getPivotTableFieldOptions(name, opts.Columns) var items []*xlsxItem if !ok || !columnOptions.DefaultSubtotal { items = append(items, &xlsxItem{X: &x}) @@ -574,9 +574,9 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio items = append(items, &xlsxItem{T: "default"}) } pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ - Name: f.getPivotTableFieldName(name, opt.Columns), + Name: f.getPivotTableFieldName(name, opts.Columns), Axis: "axisCol", - DataField: inPivotTableField(opt.Data, name) != -1, + DataField: inPivotTableField(opts.Data, name) != -1, Compact: &columnOptions.Compact, Outline: &columnOptions.Outline, DefaultSubtotal: &columnOptions.DefaultSubtotal, @@ -587,7 +587,7 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio }) continue } - if inPivotTableField(opt.Data, name) != -1 { + if inPivotTableField(opts.Data, name) != -1 { pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ DataField: true, }) @@ -626,9 +626,9 @@ func (f *File) countPivotCache() int { // getPivotFieldsIndex convert the column of the first row in the data region // to a sequential index by given fields and pivot option. -func (f *File) getPivotFieldsIndex(fields []PivotTableField, opt *PivotTableOption) ([]int, error) { +func (f *File) getPivotFieldsIndex(fields []PivotTableField, opts *PivotTableOption) ([]int, error) { var pivotFieldsIndex []int - orders, err := f.getPivotFieldsOrder(opt) + orders, err := f.getPivotFieldsOrder(opts) if err != nil { return pivotFieldsIndex, err } diff --git a/rows_test.go b/rows_test.go index 02e2d2071c..d51e256857 100644 --- a/rows_test.go +++ b/rows_test.go @@ -880,7 +880,7 @@ func TestDuplicateRowTo(t *testing.T) { assert.Equal(t, nil, f.DuplicateRowTo(sheetName, 1, 1)) // Test duplicate row on the blank worksheet assert.Equal(t, nil, f.DuplicateRowTo(sheetName, 1, 2)) - // Test duplicate row on the worksheet with illegal cell coordinates + // Test duplicate row on the worksheet with illegal cell reference f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:B1"}}}, }) diff --git a/sheet.go b/sheet.go index a62675857a..ee4f667553 100644 --- a/sheet.go +++ b/sheet.go @@ -329,23 +329,23 @@ func (f *File) getActiveSheetID() int { return 0 } -// SetSheetName provides a function to set the worksheet name by given old and -// new worksheet names. Maximum 31 characters are allowed in sheet title and +// SetSheetName provides a function to set the worksheet name by given source and +// target worksheet names. Maximum 31 characters are allowed in sheet title and // this function only changes the name of the sheet and will not update the // sheet name in the formula or reference associated with the cell. So there // may be problem formula error or reference missing. -func (f *File) SetSheetName(oldName, newName string) { - oldName = trimSheetName(oldName) - newName = trimSheetName(newName) - if strings.EqualFold(newName, oldName) { +func (f *File) SetSheetName(source, target string) { + source = trimSheetName(source) + target = trimSheetName(target) + if strings.EqualFold(target, source) { return } content := f.workbookReader() for k, v := range content.Sheets.Sheet { - if v.Name == oldName { - content.Sheets.Sheet[k].Name = newName - f.sheetMap[newName] = f.sheetMap[oldName] - delete(f.sheetMap, oldName) + if v.Name == source { + content.Sheets.Sheet[k].Name = target + f.sheetMap[target] = f.sheetMap[source] + delete(f.sheetMap, source) } } } @@ -815,17 +815,17 @@ func (f *File) GetSheetVisible(sheet string) bool { return visible } -// SearchSheet provides a function to get coordinates by given worksheet name, +// SearchSheet provides a function to get cell reference by given worksheet name, // cell value, and regular expression. The function doesn't support searching // on the calculated result, formatted numbers and conditional lookup -// currently. If it is a merged cell, it will return the coordinates of the -// upper left corner of the merged area. +// currently. If it is a merged cell, it will return the cell reference of the +// upper left cell of the merged range reference. // -// An example of search the coordinates of the value of "100" on Sheet1: +// An example of search the cell reference of the value of "100" on Sheet1: // // result, err := f.SearchSheet("Sheet1", "100") // -// An example of search the coordinates where the numerical value in the range +// An example of search the cell reference where the numerical value in the range // of "0-9" of Sheet1 is described: // // result, err := f.SearchSheet("Sheet1", "[0-9]", true) @@ -849,8 +849,8 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { return f.searchSheet(name, value, regSearch) } -// searchSheet provides a function to get coordinates by given worksheet name, -// cell value, and regular expression. +// searchSheet provides a function to get cell reference by given worksheet +// name, cell value, and regular expression. func (f *File) searchSheet(name, value string, regSearch bool) (result []string, err error) { var ( cellName, inElement string @@ -1684,7 +1684,7 @@ func (f *File) UngroupSheets() error { } // InsertPageBreak create a page break to determine where the printed page -// ends and where begins the next one by given worksheet name and axis, so the +// ends and where begins the next one by given worksheet name and cell reference, so the // content before the page break will be printed on one page and after the // page break on another. func (f *File) InsertPageBreak(sheet, cell string) (err error) { @@ -1741,7 +1741,8 @@ func (f *File) InsertPageBreak(sheet, cell string) (err error) { return } -// RemovePageBreak remove a page break by given worksheet name and axis. +// RemovePageBreak remove a page break by given worksheet name and cell +// reference. func (f *File) RemovePageBreak(sheet, cell string) (err error) { var ws *xlsxWorksheet var row, col int diff --git a/sheet_test.go b/sheet_test.go index 4df62bff65..324d626bfd 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -130,8 +130,8 @@ func TestPageLayoutOption(t *testing.T) { for i, test := range testData { t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - opt := test.nonDefault - t.Logf("option %T", opt) + opts := test.nonDefault + t.Logf("option %T", opts) def := deepcopy.Copy(test.container).(PageLayoutOptionPtr) val1 := deepcopy.Copy(def).(PageLayoutOptionPtr) @@ -139,34 +139,34 @@ func TestPageLayoutOption(t *testing.T) { f := NewFile() // Get the default value - assert.NoError(t, f.GetPageLayout(sheet, def), opt) + assert.NoError(t, f.GetPageLayout(sheet, def), opts) // Get again and check - assert.NoError(t, f.GetPageLayout(sheet, val1), opt) - if !assert.Equal(t, val1, def, opt) { + assert.NoError(t, f.GetPageLayout(sheet, val1), opts) + if !assert.Equal(t, val1, def, opts) { t.FailNow() } // Set the same value - assert.NoError(t, f.SetPageLayout(sheet, val1), opt) + assert.NoError(t, f.SetPageLayout(sheet, val1), opts) // Get again and check - assert.NoError(t, f.GetPageLayout(sheet, val1), opt) - if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { + assert.NoError(t, f.GetPageLayout(sheet, val1), opts) + if !assert.Equal(t, val1, def, "%T: value should not have changed", opts) { t.FailNow() } // Set a different value - assert.NoError(t, f.SetPageLayout(sheet, test.nonDefault), opt) - assert.NoError(t, f.GetPageLayout(sheet, val1), opt) + assert.NoError(t, f.SetPageLayout(sheet, test.nonDefault), opts) + assert.NoError(t, f.GetPageLayout(sheet, val1), opts) // Get again and compare - assert.NoError(t, f.GetPageLayout(sheet, val2), opt) - if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { + assert.NoError(t, f.GetPageLayout(sheet, val2), opts) + if !assert.Equal(t, val1, val2, "%T: value should not have changed", opts) { t.FailNow() } // Value should not be the same as the default - if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) { + if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opts) { t.FailNow() } // Restore the default value - assert.NoError(t, f.SetPageLayout(sheet, def), opt) - assert.NoError(t, f.GetPageLayout(sheet, val1), opt) + assert.NoError(t, f.SetPageLayout(sheet, def), opts) + assert.NoError(t, f.GetPageLayout(sheet, val1), opts) if !assert.Equal(t, def, val1) { t.FailNow() } @@ -218,7 +218,7 @@ func TestSearchSheet(t *testing.T) { f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) result, err = f.SearchSheet("Sheet1", "A") - assert.EqualError(t, err, "invalid cell coordinates [1, 0]") + assert.EqualError(t, err, "invalid cell reference [1, 0]") assert.Equal(t, []string(nil), result) } diff --git a/sheetpr_test.go b/sheetpr_test.go index 047e5f1d3c..291866806e 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -132,8 +132,8 @@ func TestSheetPrOptions(t *testing.T) { for i, test := range testData { t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - opt := test.nonDefault - t.Logf("option %T", opt) + opts := test.nonDefault + t.Logf("option %T", opts) def := deepcopy.Copy(test.container).(SheetPrOptionPtr) val1 := deepcopy.Copy(def).(SheetPrOptionPtr) @@ -141,34 +141,34 @@ func TestSheetPrOptions(t *testing.T) { f := NewFile() // Get the default value - assert.NoError(t, f.GetSheetPrOptions(sheet, def), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, def), opts) // Get again and check - assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) - if !assert.Equal(t, val1, def, opt) { + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opts) + if !assert.Equal(t, val1, def, opts) { t.FailNow() } // Set the same value - assert.NoError(t, f.SetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.SetSheetPrOptions(sheet, val1), opts) // Get again and check - assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) - if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opts) + if !assert.Equal(t, val1, def, "%T: value should not have changed", opts) { t.FailNow() } // Set a different value - assert.NoError(t, f.SetSheetPrOptions(sheet, test.nonDefault), opt) - assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.SetSheetPrOptions(sheet, test.nonDefault), opts) + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opts) // Get again and compare - assert.NoError(t, f.GetSheetPrOptions(sheet, val2), opt) - if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { + assert.NoError(t, f.GetSheetPrOptions(sheet, val2), opts) + if !assert.Equal(t, val1, val2, "%T: value should not have changed", opts) { t.FailNow() } // Value should not be the same as the default - if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) { + if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opts) { t.FailNow() } // Restore the default value - assert.NoError(t, f.SetSheetPrOptions(sheet, def), opt) - assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.SetSheetPrOptions(sheet, def), opts) + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opts) if !assert.Equal(t, def, val1) { t.FailNow() } @@ -281,8 +281,8 @@ func TestPageMarginsOption(t *testing.T) { for i, test := range testData { t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - opt := test.nonDefault - t.Logf("option %T", opt) + opts := test.nonDefault + t.Logf("option %T", opts) def := deepcopy.Copy(test.container).(PageMarginsOptionsPtr) val1 := deepcopy.Copy(def).(PageMarginsOptionsPtr) @@ -290,34 +290,34 @@ func TestPageMarginsOption(t *testing.T) { f := NewFile() // Get the default value - assert.NoError(t, f.GetPageMargins(sheet, def), opt) + assert.NoError(t, f.GetPageMargins(sheet, def), opts) // Get again and check - assert.NoError(t, f.GetPageMargins(sheet, val1), opt) - if !assert.Equal(t, val1, def, opt) { + assert.NoError(t, f.GetPageMargins(sheet, val1), opts) + if !assert.Equal(t, val1, def, opts) { t.FailNow() } // Set the same value - assert.NoError(t, f.SetPageMargins(sheet, val1), opt) + assert.NoError(t, f.SetPageMargins(sheet, val1), opts) // Get again and check - assert.NoError(t, f.GetPageMargins(sheet, val1), opt) - if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { + assert.NoError(t, f.GetPageMargins(sheet, val1), opts) + if !assert.Equal(t, val1, def, "%T: value should not have changed", opts) { t.FailNow() } // Set a different value - assert.NoError(t, f.SetPageMargins(sheet, test.nonDefault), opt) - assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + assert.NoError(t, f.SetPageMargins(sheet, test.nonDefault), opts) + assert.NoError(t, f.GetPageMargins(sheet, val1), opts) // Get again and compare - assert.NoError(t, f.GetPageMargins(sheet, val2), opt) - if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { + assert.NoError(t, f.GetPageMargins(sheet, val2), opts) + if !assert.Equal(t, val1, val2, "%T: value should not have changed", opts) { t.FailNow() } // Value should not be the same as the default - if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) { + if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opts) { t.FailNow() } // Restore the default value - assert.NoError(t, f.SetPageMargins(sheet, def), opt) - assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + assert.NoError(t, f.SetPageMargins(sheet, def), opts) + assert.NoError(t, f.GetPageMargins(sheet, val1), opts) if !assert.Equal(t, def, val1) { t.FailNow() } @@ -417,8 +417,8 @@ func TestSheetFormatPrOptions(t *testing.T) { for i, test := range testData { t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - opt := test.nonDefault - t.Logf("option %T", opt) + opts := test.nonDefault + t.Logf("option %T", opts) def := deepcopy.Copy(test.container).(SheetFormatPrOptionsPtr) val1 := deepcopy.Copy(def).(SheetFormatPrOptionsPtr) @@ -426,34 +426,34 @@ func TestSheetFormatPrOptions(t *testing.T) { f := NewFile() // Get the default value - assert.NoError(t, f.GetSheetFormatPr(sheet, def), opt) + assert.NoError(t, f.GetSheetFormatPr(sheet, def), opts) // Get again and check - assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt) - if !assert.Equal(t, val1, def, opt) { + assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opts) + if !assert.Equal(t, val1, def, opts) { t.FailNow() } // Set the same value - assert.NoError(t, f.SetSheetFormatPr(sheet, val1), opt) + assert.NoError(t, f.SetSheetFormatPr(sheet, val1), opts) // Get again and check - assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt) - if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { + assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opts) + if !assert.Equal(t, val1, def, "%T: value should not have changed", opts) { t.FailNow() } // Set a different value - assert.NoError(t, f.SetSheetFormatPr(sheet, test.nonDefault), opt) - assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt) + assert.NoError(t, f.SetSheetFormatPr(sheet, test.nonDefault), opts) + assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opts) // Get again and compare - assert.NoError(t, f.GetSheetFormatPr(sheet, val2), opt) - if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { + assert.NoError(t, f.GetSheetFormatPr(sheet, val2), opts) + if !assert.Equal(t, val1, val2, "%T: value should not have changed", opts) { t.FailNow() } // Value should not be the same as the default - if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) { + if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opts) { t.FailNow() } // Restore the default value - assert.NoError(t, f.SetSheetFormatPr(sheet, def), opt) - assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt) + assert.NoError(t, f.SetSheetFormatPr(sheet, def), opts) + assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opts) if !assert.Equal(t, def, val1) { t.FailNow() } diff --git a/sparkline.go b/sparkline.go index bf1e09cd94..79cb1f2b71 100644 --- a/sparkline.go +++ b/sparkline.go @@ -387,7 +387,7 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // Markers | Toggle sparkline markers // ColorAxis | An RGB Color is specified as RRGGBB // Axis | Show sparkline axis -func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { +func (f *File) AddSparkline(sheet string, opts *SparklineOption) (err error) { var ( ws *xlsxWorksheet sparkType string @@ -400,39 +400,39 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { ) // parameter validation - if ws, err = f.parseFormatAddSparklineSet(sheet, opt); err != nil { + if ws, err = f.parseFormatAddSparklineSet(sheet, opts); err != nil { return } // Handle the sparkline type sparkType = "line" sparkTypes = map[string]string{"line": "line", "column": "column", "win_loss": "stacked"} - if opt.Type != "" { - if specifiedSparkTypes, ok = sparkTypes[opt.Type]; !ok { + if opts.Type != "" { + if specifiedSparkTypes, ok = sparkTypes[opts.Type]; !ok { err = ErrSparklineType return } sparkType = specifiedSparkTypes } - group = f.addSparklineGroupByStyle(opt.Style) + group = f.addSparklineGroupByStyle(opts.Style) group.Type = sparkType group.ColorAxis = &xlsxColor{RGB: "FF000000"} group.DisplayEmptyCellsAs = "gap" - group.High = opt.High - group.Low = opt.Low - group.First = opt.First - group.Last = opt.Last - group.Negative = opt.Negative - group.DisplayXAxis = opt.Axis - group.Markers = opt.Markers - if opt.SeriesColor != "" { + group.High = opts.High + group.Low = opts.Low + group.First = opts.First + group.Last = opts.Last + group.Negative = opts.Negative + group.DisplayXAxis = opts.Axis + group.Markers = opts.Markers + if opts.SeriesColor != "" { group.ColorSeries = &xlsxTabColor{ - RGB: getPaletteColor(opt.SeriesColor), + RGB: getPaletteColor(opts.SeriesColor), } } - if opt.Reverse { - group.RightToLeft = opt.Reverse + if opts.Reverse { + group.RightToLeft = opts.Reverse } - f.addSparkline(opt, group) + f.addSparkline(opts, group) if ws.ExtLst.Ext != "" { // append mode ext if err = f.appendSparkline(ws, group, groups); err != nil { return @@ -459,25 +459,25 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { // parseFormatAddSparklineSet provides a function to validate sparkline // properties. -func (f *File) parseFormatAddSparklineSet(sheet string, opt *SparklineOption) (*xlsxWorksheet, error) { +func (f *File) parseFormatAddSparklineSet(sheet string, opts *SparklineOption) (*xlsxWorksheet, error) { ws, err := f.workSheetReader(sheet) if err != nil { return ws, err } - if opt == nil { + if opts == nil { return ws, ErrParameterRequired } - if len(opt.Location) < 1 { + if len(opts.Location) < 1 { return ws, ErrSparklineLocation } - if len(opt.Range) < 1 { + if len(opts.Range) < 1 { return ws, ErrSparklineRange } - // The ranges and locations must match.\ - if len(opt.Location) != len(opt.Range) { + // The range and locations must match + if len(opts.Location) != len(opts.Range) { return ws, ErrSparkline } - if opt.Style < 0 || opt.Style > 35 { + if opts.Style < 0 || opts.Style > 35 { return ws, ErrSparklineStyle } if ws.ExtLst == nil { @@ -488,10 +488,10 @@ func (f *File) parseFormatAddSparklineSet(sheet string, opt *SparklineOption) (* // addSparkline provides a function to create a sparkline in a sparkline group // by given properties. -func (f *File) addSparkline(opt *SparklineOption, group *xlsxX14SparklineGroup) { - for idx, location := range opt.Location { +func (f *File) addSparkline(opts *SparklineOption, group *xlsxX14SparklineGroup) { + for idx, location := range opts.Location { group.Sparklines.Sparkline = append(group.Sparklines.Sparkline, &xlsxX14Sparkline{ - F: opt.Range[idx], + F: opts.Range[idx], Sqref: location, }) } diff --git a/stream.go b/stream.go index 3c05c327af..6c2f6a2f63 100644 --- a/stream.go +++ b/stream.go @@ -117,7 +117,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { } // AddTable creates an Excel table for the StreamWriter using the given -// coordinate area and format set. For example, create a table of A1:D5: +// cell range and format set. For example, create a table of A1:D5: // // err := sw.AddTable("A1", "D5", "") // @@ -156,7 +156,7 @@ func (sw *StreamWriter) AddTable(hCell, vCell, format string) error { coordinates[3]++ } - // Correct table reference coordinate area, such correct C1:B3 to B1:C3. + // Correct table reference range, such correct C1:B3 to B1:C3. ref, err := sw.File.coordinatesToAreaRef(coordinates) if err != nil { return err @@ -308,8 +308,8 @@ type RowOpts struct { // // As a special case, if Cell is used as a value, then the Cell.StyleID will be // applied to that cell. -func (sw *StreamWriter) SetRow(axis string, values []interface{}, opts ...RowOpts) error { - col, row, err := CellNameToCoordinates(axis) +func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpts) error { + col, row, err := CellNameToCoordinates(cell) if err != nil { return err } @@ -329,11 +329,11 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}, opts ...RowOpt if val == nil { continue } - axis, err := CoordinatesToCellName(col+i, row) + ref, err := CoordinatesToCellName(col+i, row) if err != nil { return err } - c := xlsxC{R: axis} + c := xlsxC{R: ref} if v, ok := val.(Cell); ok { c.S = v.StyleID val = v.Value @@ -355,24 +355,24 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}, opts ...RowOpt // marshalRowAttrs prepare attributes of the row by given options. func marshalRowAttrs(opts ...RowOpts) (attrs string, err error) { - var opt *RowOpts + var options *RowOpts for i := range opts { - opt = &opts[i] + options = &opts[i] } - if opt == nil { + if options == nil { return } - if opt.Height > MaxRowHeight { + if options.Height > MaxRowHeight { err = ErrMaxRowHeight return } - if opt.StyleID > 0 { - attrs += fmt.Sprintf(` s="%d" customFormat="true"`, opt.StyleID) + if options.StyleID > 0 { + attrs += fmt.Sprintf(` s="%d" customFormat="true"`, options.StyleID) } - if opt.Height > 0 { - attrs += fmt.Sprintf(` ht="%v" customHeight="true"`, opt.Height) + if options.Height > 0 { + attrs += fmt.Sprintf(` ht="%v" customHeight="true"`, options.Height) } - if opt.Hidden { + if options.Hidden { attrs += ` hidden="true"` } return @@ -401,7 +401,7 @@ func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { return nil } -// MergeCell provides a function to merge cells by a given coordinate area for +// MergeCell provides a function to merge cells by a given range reference for // the StreamWriter. Don't create a merged cell that overlaps with another // existing merged cell. func (sw *StreamWriter) MergeCell(hCell, vCell string) error { diff --git a/stream_test.go b/stream_test.go index 029082ca34..d935c7bcb3 100644 --- a/stream_test.go +++ b/stream_test.go @@ -175,7 +175,7 @@ func TestStreamTable(t *testing.T) { // Test add table with illegal formatset. assert.EqualError(t, streamWriter.AddTable("B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") - // Test add table with illegal cell coordinates. + // Test add table with illegal cell reference. assert.EqualError(t, streamWriter.AddTable("A", "B1", `{}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, streamWriter.AddTable("A1", "B", `{}`), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) } @@ -185,7 +185,7 @@ func TestStreamMergeCells(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.MergeCell("A1", "D1")) - // Test merge cells with illegal cell coordinates. + // Test merge cells with illegal cell reference. assert.EqualError(t, streamWriter.MergeCell("A", "D1"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, streamWriter.Flush()) // Save spreadsheet by the given path. diff --git a/styles.go b/styles.go index ded7c30733..587fd749a5 100644 --- a/styles.go +++ b/styles.go @@ -1992,9 +1992,9 @@ func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (styleID int) { } // NewConditionalStyle provides a function to create style for conditional -// format by given style format. The parameters are the same as function -// NewStyle(). Note that the color field uses RGB color code and only support -// to set font, fills, alignment and borders currently. +// format by given style format. The parameters are the same with the NewStyle +// function. Note that the color field uses RGB color code and only support to +// set font, fills, alignment and borders currently. func (f *File) NewConditionalStyle(style string) (int, error) { s := f.stylesReader() fs, err := parseFormatStyleSet(style) @@ -2480,23 +2480,23 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a } // GetCellStyle provides a function to get cell style index by given worksheet -// name and cell coordinates. -func (f *File) GetCellStyle(sheet, axis string) (int, error) { +// name and cell reference. +func (f *File) GetCellStyle(sheet, cell string) (int, error) { ws, err := f.workSheetReader(sheet) if err != nil { return 0, err } - cellData, col, row, err := f.prepareCell(ws, axis) + c, col, row, err := f.prepareCell(ws, cell) if err != nil { return 0, err } - return f.prepareCellStyle(ws, col, row, cellData.S), err + return f.prepareCellStyle(ws, col, row, c.S), err } // SetCellStyle provides a function to add style attribute for cells by given -// worksheet name, coordinate area and style ID. This function is concurrency +// worksheet name, range reference and style ID. This function is concurrency // safe. Note that diagonalDown and diagonalUp type border should be use same -// color in the same coordinate area. SetCellStyle will overwrite the existing +// color in the same range. SetCellStyle will overwrite the existing // styles for the cell, it won't append or merge style with existing styles. // // For example create a borders of cell H9 on Sheet1: @@ -2607,7 +2607,7 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { return err } - // Normalize the coordinate area, such correct C1:B3 to B1:C3. + // Normalize the range, such correct C1:B3 to B1:C3. if vCol < hCol { vCol, hCol = hCol, vCol } @@ -3050,14 +3050,14 @@ func (f *File) GetConditionalFormats(sheet string) (map[string]string, error) { } // UnsetConditionalFormat provides a function to unset the conditional format -// by given worksheet name and range. -func (f *File) UnsetConditionalFormat(sheet, area string) error { +// by given worksheet name and range reference. +func (f *File) UnsetConditionalFormat(sheet, reference string) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } for i, cf := range ws.ConditionalFormatting { - if cf.SQRef == area { + if cf.SQRef == reference { ws.ConditionalFormatting = append(ws.ConditionalFormatting[:i], ws.ConditionalFormatting[i+1:]...) return nil } diff --git a/table.go b/table.go index dc5f441970..66ba72972a 100644 --- a/table.go +++ b/table.go @@ -29,7 +29,7 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) { } // AddTable provides the method to add table in a worksheet by given worksheet -// name, coordinate area and format set. For example, create a table of A1:D5 +// name, range reference and format set. For example, create a table of A1:D5 // on Sheet1: // // err := f.AddTable("Sheet1", "A1", "D5", "") @@ -159,14 +159,14 @@ func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn, } // addTable provides a function to add table by given worksheet name, -// coordinate area and format set. +// range reference and format set. func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet *formatTable) error { // Correct the minimum number of rows, the table at least two lines. if y1 == y2 { y2++ } - // Correct table reference coordinate area, such correct C1:B3 to B1:C3. + // Correct table range reference, such correct C1:B3 to B1:C3. ref, err := f.coordinatesToAreaRef([]int{x1, y1, x2, y2}) if err != nil { return err @@ -211,17 +211,17 @@ func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { } // AutoFilter provides the method to add auto filter in a worksheet by given -// worksheet name, coordinate area and settings. An autofilter in Excel is a +// worksheet name, range reference and settings. An auto filter in Excel is a // way of filtering a 2D range of data based on some simple criteria. For -// example applying an autofilter to a cell range A1:D4 in the Sheet1: +// example applying an auto filter to a cell range A1:D4 in the Sheet1: // // err := f.AutoFilter("Sheet1", "A1", "D4", "") // -// Filter data in an autofilter: +// Filter data in an auto filter: // // err := f.AutoFilter("Sheet1", "A1", "D4", `{"column":"B","expression":"x != blanks"}`) // -// column defines the filter columns in a autofilter range based on simple +// column defines the filter columns in a auto filter range based on simple // criteria // // It isn't sufficient to just specify the filter condition. You must also diff --git a/table_test.go b/table_test.go index 39b418ba7c..c997ad243b 100644 --- a/table_test.go +++ b/table_test.go @@ -33,22 +33,22 @@ func TestAddTable(t *testing.T) { assert.EqualError(t, f.AddTable("SheetN", "B26", "A21", `{}`), "sheet SheetN does not exist") // Test add table with illegal formatset. assert.EqualError(t, f.AddTable("Sheet1", "B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") - // Test add table with illegal cell coordinates. + // Test add table with illegal cell reference. assert.EqualError(t, f.AddTable("Sheet1", "A", "B1", `{}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.AddTable("Sheet1", "A1", "B", `{}`), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) - // Test addTable with illegal cell coordinates. + // Test addTable with illegal cell reference. f = NewFile() - assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell coordinates [0, 0]") - assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell coordinates [0, 0]") + assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell reference [0, 0]") + assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell reference [0, 0]") } func TestSetTableHeader(t *testing.T) { f := NewFile() _, err := f.setTableHeader("Sheet1", 1, 0, 1) - assert.EqualError(t, err, "invalid cell coordinates [1, 0]") + assert.EqualError(t, err, "invalid cell reference [1, 0]") } func TestAutoFilter(t *testing.T) { @@ -78,7 +78,7 @@ func TestAutoFilter(t *testing.T) { }) } - // Test AutoFilter with illegal cell coordinates. + // Test AutoFilter with illegal cell reference. assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) } diff --git a/xmlApp.go b/xmlApp.go index abfd82b3d7..f21e5f952f 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -15,13 +15,13 @@ import "encoding/xml" // AppProperties directly maps the document application properties. type AppProperties struct { - Application string - ScaleCrop bool - DocSecurity int - Company string - LinksUpToDate bool - HyperlinksChanged bool - AppVersion string + Application string `json:"application"` + ScaleCrop bool `json:"scale_crop"` + DocSecurity int `json:"doc_security"` + Company string `json:"company"` + LinksUpToDate bool `json:"links_up_to_date"` + HyperlinksChanged bool `json:"hyperlinks_changed"` + AppVersion string `json:"app_version"` } // xlsxProperties specifies to an OOXML document properties such as the From 74dad51cfce19c2f67a0ed9fe1479b6d21d767e9 Mon Sep 17 00:00:00 2001 From: invzhi Date: Wed, 21 Sep 2022 00:29:34 +0800 Subject: [PATCH 106/213] This closes #1354, stream writer will apply style in `RowOpts` for each cell (#1355) Co-authored-by: Tianzhi Jin --- stream.go | 63 ++++++++++++++++++++++++++++---------------------- stream_test.go | 44 +++++++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 32 deletions(-) diff --git a/stream.go b/stream.go index 6c2f6a2f63..0cffe45b2d 100644 --- a/stream.go +++ b/stream.go @@ -302,6 +302,37 @@ type RowOpts struct { StyleID int } +// marshalAttrs prepare attributes of the row. +func (r *RowOpts) marshalAttrs() (attrs string, err error) { + if r == nil { + return + } + if r.Height > MaxRowHeight { + err = ErrMaxRowHeight + return + } + if r.StyleID > 0 { + attrs += fmt.Sprintf(` s="%d" customFormat="true"`, r.StyleID) + } + if r.Height > 0 { + attrs += fmt.Sprintf(` ht="%v" customHeight="true"`, r.Height) + } + if r.Hidden { + attrs += ` hidden="true"` + } + return +} + +// parseRowOpts provides a function to parse the optional settings for +// *StreamWriter.SetRow. +func parseRowOpts(opts ...RowOpts) *RowOpts { + options := &RowOpts{} + for _, opt := range opts { + options = &opt + } + return options +} + // SetRow writes an array to stream rows by giving a worksheet name, starting // coordinate and a pointer to an array of values. Note that you must call the // 'Flush' method to end the streaming writing process. @@ -320,11 +351,12 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt _, _ = sw.rawData.WriteString(``) sw.sheetWritten = true } - attrs, err := marshalRowAttrs(opts...) + options := parseRowOpts(opts...) + attrs, err := options.marshalAttrs() if err != nil { return err } - fmt.Fprintf(&sw.rawData, ``, row, attrs) + _, _ = fmt.Fprintf(&sw.rawData, ``, row, attrs) for i, val := range values { if val == nil { continue @@ -333,7 +365,7 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt if err != nil { return err } - c := xlsxC{R: ref} + c := xlsxC{R: ref, S: options.StyleID} if v, ok := val.(Cell); ok { c.S = v.StyleID val = v.Value @@ -353,31 +385,6 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt return sw.rawData.Sync() } -// marshalRowAttrs prepare attributes of the row by given options. -func marshalRowAttrs(opts ...RowOpts) (attrs string, err error) { - var options *RowOpts - for i := range opts { - options = &opts[i] - } - if options == nil { - return - } - if options.Height > MaxRowHeight { - err = ErrMaxRowHeight - return - } - if options.StyleID > 0 { - attrs += fmt.Sprintf(` s="%d" customFormat="true"`, options.StyleID) - } - if options.Height > 0 { - attrs += fmt.Sprintf(` ht="%v" customHeight="true"`, options.Height) - } - if options.Hidden { - attrs += ` hidden="true"` - } - return -} - // SetColWidth provides a function to set the width of a single column or // multiple columns for the StreamWriter. Note that you must call // the 'SetColWidth' function before the 'SetRow' function. For example set diff --git a/stream_test.go b/stream_test.go index d935c7bcb3..1026cb3492 100644 --- a/stream_test.go +++ b/stream_test.go @@ -55,7 +55,7 @@ func TestStreamWriter(t *testing.T) { // Test set cell with style. styleID, err := file.NewStyle(&Style{Font: &Font{Color: "#777777"}}) assert.NoError(t, err) - assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}, Cell{Formula: "SUM(A10,B10)"}}), RowOpts{Height: 45, StyleID: styleID}) + assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}, Cell{Formula: "SUM(A10,B10)"}}, RowOpts{Height: 45, StyleID: styleID})) assert.NoError(t, streamWriter.SetRow("A5", []interface{}{&Cell{StyleID: styleID, Value: "cell"}, &Cell{Formula: "SUM(A10,B10)"}})) assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()})) assert.NoError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: 20, Hidden: true, StyleID: styleID})) @@ -201,7 +201,14 @@ func TestNewStreamWriter(t *testing.T) { assert.EqualError(t, err, "sheet SheetN does not exist") } -func TestSetRow(t *testing.T) { +func TestStreamMarshalAttrs(t *testing.T) { + var r *RowOpts + attrs, err := r.marshalAttrs() + assert.NoError(t, err) + assert.Empty(t, attrs) +} + +func TestStreamSetRow(t *testing.T) { // Test error exceptions file := NewFile() streamWriter, err := file.NewStreamWriter("Sheet1") @@ -209,7 +216,7 @@ func TestSetRow(t *testing.T) { assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } -func TestSetRowNilValues(t *testing.T) { +func TestStreamSetRowNilValues(t *testing.T) { file := NewFile() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) @@ -220,7 +227,36 @@ func TestSetRowNilValues(t *testing.T) { assert.NotEqual(t, ws.SheetData.Row[0].C[0].XMLName.Local, "c") } -func TestSetCellValFunc(t *testing.T) { +func TestStreamSetRowWithStyle(t *testing.T) { + file := NewFile() + zeroStyleID := 0 + grayStyleID, err := file.NewStyle(&Style{Font: &Font{Color: "#777777"}}) + assert.NoError(t, err) + blueStyleID, err := file.NewStyle(&Style{Font: &Font{Color: "#0000FF"}}) + assert.NoError(t, err) + + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + assert.NoError(t, streamWriter.SetRow("A1", []interface{}{ + "value1", + Cell{Value: "value2"}, + &Cell{Value: "value2"}, + Cell{StyleID: blueStyleID, Value: "value3"}, + &Cell{StyleID: blueStyleID, Value: "value3"}, + }, RowOpts{StyleID: grayStyleID})) + err = streamWriter.Flush() + assert.NoError(t, err) + + ws, err := file.workSheetReader("Sheet1") + assert.NoError(t, err) + assert.Equal(t, grayStyleID, ws.SheetData.Row[0].C[0].S) + assert.Equal(t, zeroStyleID, ws.SheetData.Row[0].C[1].S) + assert.Equal(t, zeroStyleID, ws.SheetData.Row[0].C[2].S) + assert.Equal(t, blueStyleID, ws.SheetData.Row[0].C[3].S) + assert.Equal(t, blueStyleID, ws.SheetData.Row[0].C[4].S) +} + +func TestStreamSetCellValFunc(t *testing.T) { f := NewFile() sw, err := f.NewStreamWriter("Sheet1") assert.NoError(t, err) From addcc1a0b257d3b71e33891891c3a3df4d34f0dc Mon Sep 17 00:00:00 2001 From: Zitao <369815332@qq.com> Date: Fri, 23 Sep 2022 00:02:45 +0800 Subject: [PATCH 107/213] Fix cpu usage problem of stream writer when merging cells (#1359) Co-authored-by: zzt --- stream.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/stream.go b/stream.go index 0cffe45b2d..84299cbf31 100644 --- a/stream.go +++ b/stream.go @@ -34,7 +34,7 @@ type StreamWriter struct { worksheet *xlsxWorksheet rawData bufferedWriter mergeCellsCount int - mergeCells string + mergeCells strings.Builder tableParts string } @@ -417,7 +417,11 @@ func (sw *StreamWriter) MergeCell(hCell, vCell string) error { return err } sw.mergeCellsCount++ - sw.mergeCells += fmt.Sprintf(``, hCell, vCell) + _, _ = sw.mergeCells.WriteString(``) return nil } @@ -526,10 +530,15 @@ func (sw *StreamWriter) Flush() error { } _, _ = sw.rawData.WriteString(``) bulkAppendFields(&sw.rawData, sw.worksheet, 8, 15) + mergeCells := strings.Builder{} if sw.mergeCellsCount > 0 { - sw.mergeCells = fmt.Sprintf(`%s`, sw.mergeCellsCount, sw.mergeCells) + _, _ = mergeCells.WriteString(``) + _, _ = mergeCells.WriteString(sw.mergeCells.String()) + _, _ = mergeCells.WriteString(``) } - _, _ = sw.rawData.WriteString(sw.mergeCells) + _, _ = sw.rawData.WriteString(mergeCells.String()) bulkAppendFields(&sw.rawData, sw.worksheet, 17, 38) _, _ = sw.rawData.WriteString(sw.tableParts) bulkAppendFields(&sw.rawData, sw.worksheet, 40, 40) From efcf599dfe2ec25f10c4d55513a5648addfe989b Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 28 Sep 2022 00:04:17 +0800 Subject: [PATCH 108/213] This closes #1360, closes #1361 - Fix default number format parse issue with a long string of digits - Fix creating a sheet with an empty name cause a corrupted file - The `GetCellStyle` function no longer return master cell style of the merge cell range - Using the specialized name in variables and functions --- README.md | 2 +- adjust.go | 16 +++++++------- calc.go | 20 ++++++++++------- calc_test.go | 2 +- cell.go | 23 ++++++++++---------- cell_test.go | 38 +++++++++++++++++--------------- col.go | 2 +- datavalidation.go | 4 ++-- excelize_test.go | 2 +- lib.go | 55 ++++++++++++++++------------------------------- lib_test.go | 10 ++++----- merge.go | 12 +++++------ merge_test.go | 2 +- numfmt.go | 12 +++++------ picture.go | 4 ++-- pivotTable.go | 7 +++--- rows.go | 30 ++++++-------------------- rows_test.go | 4 ---- sheet.go | 3 +++ sheet_test.go | 2 ++ stream.go | 6 +++--- styles.go | 11 ++++++---- table.go | 4 ++-- 23 files changed, 126 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index 89d2d001e3..6ab549edf8 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ func main() { fmt.Println(err) } }() - // Get value from cell by given worksheet name and axis. + // Get value from cell by given worksheet name and cell reference. cell, err := f.GetCellValue("Sheet1", "B2") if err != nil { fmt.Println(err) diff --git a/adjust.go b/adjust.go index 3a0271da5f..92efcd0af3 100644 --- a/adjust.go +++ b/adjust.go @@ -185,7 +185,7 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, Decode(&t); err != nil && err != io.EOF { return } - coordinates, err := areaRefToCoordinates(t.Ref) + coordinates, err := rangeRefToCoordinates(t.Ref) if err != nil { return } @@ -204,7 +204,7 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, idx-- continue } - t.Ref, _ = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}) + t.Ref, _ = f.coordinatesToRangeRef([]int{x1, y1, x2, y2}) if t.AutoFilter != nil { t.AutoFilter.Ref = t.Ref } @@ -221,7 +221,7 @@ func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, off return nil } - coordinates, err := areaRefToCoordinates(ws.AutoFilter.Ref) + coordinates, err := rangeRefToCoordinates(ws.AutoFilter.Ref) if err != nil { return err } @@ -241,7 +241,7 @@ func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, off coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset) x1, y1, x2, y2 = coordinates[0], coordinates[1], coordinates[2], coordinates[3] - if ws.AutoFilter.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { + if ws.AutoFilter.Ref, err = f.coordinatesToRangeRef([]int{x1, y1, x2, y2}); err != nil { return err } return nil @@ -277,8 +277,8 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off } for i := 0; i < len(ws.MergeCells.Cells); i++ { - areaData := ws.MergeCells.Cells[i] - coordinates, err := areaRefToCoordinates(areaData.Ref) + mergedCells := ws.MergeCells.Cells[i] + coordinates, err := rangeRefToCoordinates(mergedCells.Ref) if err != nil { return err } @@ -305,8 +305,8 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off i-- continue } - areaData.rect = []int{x1, y1, x2, y2} - if areaData.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { + mergedCells.rect = []int{x1, y1, x2, y2} + if mergedCells.Ref, err = f.coordinatesToRangeRef([]int{x1, y1, x2, y2}); err != nil { return err } } diff --git a/calc.go b/calc.go index f7b3a6300a..b19dba749c 100644 --- a/calc.go +++ b/calc.go @@ -789,10 +789,14 @@ func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result strin return } result = token.Value() - isNum, precision := isNumeric(result) - if isNum && (precision > 15 || precision == 0) { - num := roundPrecision(result, -1) - result = strings.ToUpper(num) + if isNum, precision, decimal := isNumeric(result); isNum { + if precision > 15 { + result = strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64)) + return + } + if !strings.HasPrefix(result, "0") { + result = strings.ToUpper(strconv.FormatFloat(decimal, 'f', -1, 64)) + } } return } @@ -2089,13 +2093,13 @@ func (fn *formulaFuncs) COMPLEX(argsList *list.List) formulaArg { // cmplx2str replace complex number string characters. func cmplx2str(num complex128, suffix string) string { realPart, imagPart := fmt.Sprint(real(num)), fmt.Sprint(imag(num)) - isNum, i := isNumeric(realPart) + isNum, i, decimal := isNumeric(realPart) if isNum && i > 15 { - realPart = roundPrecision(realPart, -1) + realPart = strconv.FormatFloat(decimal, 'G', 15, 64) } - isNum, i = isNumeric(imagPart) + isNum, i, decimal = isNumeric(imagPart) if isNum && i > 15 { - imagPart = roundPrecision(imagPart, -1) + imagPart = strconv.FormatFloat(decimal, 'G', 15, 64) } c := realPart if imag(num) > 0 { diff --git a/calc_test.go b/calc_test.go index ef196dca8a..df86f90743 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1736,7 +1736,7 @@ func TestCalcCellValue(t *testing.T) { "=UPPER(\"TEST 123\")": "TEST 123", // VALUE "=VALUE(\"50\")": "50", - "=VALUE(\"1.0E-07\")": "1E-07", + "=VALUE(\"1.0E-07\")": "0.0000001", "=VALUE(\"5,000\")": "5000", "=VALUE(\"20%\")": "0.2", "=VALUE(\"12:00:00\")": "0.5", diff --git a/cell.go b/cell.go index b97c4109b1..6beb3b27c0 100644 --- a/cell.go +++ b/cell.go @@ -485,7 +485,7 @@ func (f *File) SetCellDefault(sheet, cell, value string) error { // setCellDefault prepares cell type and string type cell value by a given // string. func setCellDefault(value string) (t string, v string) { - if ok, _ := isNumeric(value); !ok { + if ok, _, _ := isNumeric(value); !ok { t = "str" } v = value @@ -631,7 +631,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) // setSharedFormula set shared formula for the cells. func (ws *xlsxWorksheet) setSharedFormula(ref string) error { - coordinates, err := areaRefToCoordinates(ref) + coordinates, err := rangeRefToCoordinates(ref) if err != nil { return err } @@ -1098,7 +1098,7 @@ func (f *File) setSheetCells(sheet, cell string, slice interface{}, dir adjustDi return err } -// getCellInfo does common preparation for all SetCell* methods. +// getCellInfo does common preparation for all set cell value functions. func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, error) { var err error cell, err = f.mergeCellsParser(ws, cell) @@ -1116,8 +1116,9 @@ func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, er return &ws.SheetData.Row[row-1].C[col-1], col, row, err } -// getCellStringFunc does common value extraction workflow for all GetCell* -// methods. Passed function implements specific part of required logic. +// getCellStringFunc does common value extraction workflow for all get cell +// value function. Passed function implements specific part of required +// logic. func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool, error)) (string, error) { ws, err := f.workSheetReader(sheet) if err != nil { @@ -1235,7 +1236,7 @@ func (f *File) mergeCellsParser(ws *xlsxWorksheet, cell string) (string, error) i-- continue } - ok, err := f.checkCellInArea(cell, ws.MergeCells.Cells[i].Ref) + ok, err := f.checkCellInRangeRef(cell, ws.MergeCells.Cells[i].Ref) if err != nil { return cell, err } @@ -1247,18 +1248,18 @@ func (f *File) mergeCellsParser(ws *xlsxWorksheet, cell string) (string, error) return cell, nil } -// checkCellInArea provides a function to determine if a given cell reference +// checkCellInRangeRef provides a function to determine if a given cell reference // in a range. -func (f *File) checkCellInArea(cell, area string) (bool, error) { +func (f *File) checkCellInRangeRef(cell, reference string) (bool, error) { col, row, err := CellNameToCoordinates(cell) if err != nil { return false, err } - if rng := strings.Split(area, ":"); len(rng) != 2 { + if rng := strings.Split(reference, ":"); len(rng) != 2 { return false, err } - coordinates, err := areaRefToCoordinates(area) + coordinates, err := rangeRefToCoordinates(reference) if err != nil { return false, err } @@ -1333,7 +1334,7 @@ func parseSharedFormula(dCol, dRow int, orig []byte) (res string, start int) { // R1C1-reference notation, are the same. // // Note that this function not validate ref tag to check the cell whether in -// allow area, and always return origin shared formula. +// allow range reference, and always return origin shared formula. func getSharedFormula(ws *xlsxWorksheet, si int, cell string) string { for _, r := range ws.SheetData.Row { for _, c := range r.C { diff --git a/cell_test.go b/cell_test.go index 9c8b511d75..0205705107 100644 --- a/cell_test.go +++ b/cell_test.go @@ -95,43 +95,43 @@ func TestConcurrency(t *testing.T) { assert.NoError(t, f.Close()) } -func TestCheckCellInArea(t *testing.T) { +func TestCheckCellInRangeRef(t *testing.T) { f := NewFile() - expectedTrueCellInAreaList := [][2]string{ + expectedTrueCellInRangeRefList := [][2]string{ {"c2", "A1:AAZ32"}, {"B9", "A1:B9"}, {"C2", "C2:C2"}, } - for _, expectedTrueCellInArea := range expectedTrueCellInAreaList { - cell := expectedTrueCellInArea[0] - area := expectedTrueCellInArea[1] - ok, err := f.checkCellInArea(cell, area) + for _, expectedTrueCellInRangeRef := range expectedTrueCellInRangeRefList { + cell := expectedTrueCellInRangeRef[0] + reference := expectedTrueCellInRangeRef[1] + ok, err := f.checkCellInRangeRef(cell, reference) assert.NoError(t, err) assert.Truef(t, ok, - "Expected cell %v to be in area %v, got false\n", cell, area) + "Expected cell %v to be in range reference %v, got false\n", cell, reference) } - expectedFalseCellInAreaList := [][2]string{ + expectedFalseCellInRangeRefList := [][2]string{ {"c2", "A4:AAZ32"}, {"C4", "D6:A1"}, // weird case, but you never know {"AEF42", "BZ40:AEF41"}, } - for _, expectedFalseCellInArea := range expectedFalseCellInAreaList { - cell := expectedFalseCellInArea[0] - area := expectedFalseCellInArea[1] - ok, err := f.checkCellInArea(cell, area) + for _, expectedFalseCellInRangeRef := range expectedFalseCellInRangeRefList { + cell := expectedFalseCellInRangeRef[0] + reference := expectedFalseCellInRangeRef[1] + ok, err := f.checkCellInRangeRef(cell, reference) assert.NoError(t, err) assert.Falsef(t, ok, - "Expected cell %v not to be inside of area %v, but got true\n", cell, area) + "Expected cell %v not to be inside of range reference %v, but got true\n", cell, reference) } - ok, err := f.checkCellInArea("A1", "A:B") + ok, err := f.checkCellInRangeRef("A1", "A:B") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.False(t, ok) - ok, err = f.checkCellInArea("AA0", "Z0:AB1") + ok, err = f.checkCellInRangeRef("AA0", "Z0:AB1") assert.EqualError(t, err, newCellNameToCoordinatesError("AA0", newInvalidCellNameError("AA0")).Error()) assert.False(t, ok) } @@ -326,8 +326,10 @@ func TestGetCellValue(t *testing.T) { 275.39999999999998 68.900000000000006 1.1000000000000001 - 1234567890123_4 - 123456789_0123_4 + 1234567890123_4 + 123456789_0123_4 + +0.0000000000000000002399999999999992E-4 + 7.2399999999999992E-2 `))) f.checked = nil rows, err = f.GetRows("Sheet1") @@ -361,6 +363,8 @@ func TestGetCellValue(t *testing.T) { "1.1", "1234567890123_4", "123456789_0123_4", + "2.39999999999999E-23", + "0.0724", }}, rows) assert.NoError(t, err) } diff --git a/col.go b/col.go index adc7f85ea6..964cb9751f 100644 --- a/col.go +++ b/col.go @@ -560,7 +560,7 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) // | | | (x2,y2)| // +-----+------------+------------+ // -// Example of an object that covers some area from cell A1 to B2. +// Example of an object that covers some range reference from cell A1 to B2. // // Based on the width and height of the object we need to calculate 8 vars: // diff --git a/datavalidation.go b/datavalidation.go index 3d82f7c57c..5ae5f65932 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -333,7 +333,7 @@ func (f *File) squashSqref(cells [][]int) []string { l, r := 0, 0 for i := 1; i < len(cells); i++ { if cells[i][0] == cells[r][0] && cells[i][1]-cells[r][1] > 1 { - curr, _ := f.coordinatesToAreaRef(append(cells[l], cells[r]...)) + curr, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...)) if l == r { curr, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) } @@ -343,7 +343,7 @@ func (f *File) squashSqref(cells [][]int) []string { r++ } } - curr, _ := f.coordinatesToAreaRef(append(cells[l], cells[r]...)) + curr, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...)) if l == r { curr, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) } diff --git a/excelize_test.go b/excelize_test.go index 5756e6eafc..9bb6fa83ae 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -615,7 +615,7 @@ func TestSetCellStyleBorder(t *testing.T) { var style int - // Test set border on overlapping area with vertical variants shading styles gradient fill. + // Test set border on overlapping range with vertical variants shading styles gradient fill. style, err = f.NewStyle(&Style{ Border: []Border{ {Type: "left", Color: "0000FF", Style: 3}, diff --git a/lib.go b/lib.go index 5feb7d5951..21ce7d2ca3 100644 --- a/lib.go +++ b/lib.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "io/ioutil" + "math/big" "os" "regexp" "strconv" @@ -273,19 +274,19 @@ func CoordinatesToCellName(col, row int, abs ...bool) (string, error) { return sign + colName + sign + strconv.Itoa(row), err } -// areaRefToCoordinates provides a function to convert range reference to a +// rangeRefToCoordinates provides a function to convert range reference to a // pair of coordinates. -func areaRefToCoordinates(ref string) ([]int, error) { +func rangeRefToCoordinates(ref string) ([]int, error) { rng := strings.Split(strings.ReplaceAll(ref, "$", ""), ":") if len(rng) < 2 { return nil, ErrParameterInvalid } - return areaRangeToCoordinates(rng[0], rng[1]) + return cellRefsToCoordinates(rng[0], rng[1]) } -// areaRangeToCoordinates provides a function to convert cell range to a +// cellRefsToCoordinates provides a function to convert cell range to a // pair of coordinates. -func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) { +func cellRefsToCoordinates(firstCell, lastCell string) ([]int, error) { coordinates := make([]int, 4) var err error coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell) @@ -311,9 +312,9 @@ func sortCoordinates(coordinates []int) error { return nil } -// coordinatesToAreaRef provides a function to convert a pair of coordinates -// to area reference. -func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) { +// coordinatesToRangeRef provides a function to convert a pair of coordinates +// to range reference. +func (f *File) coordinatesToRangeRef(coordinates []int) (string, error) { if len(coordinates) != 4 { return "", ErrCoordinates } @@ -364,7 +365,7 @@ func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) { } cells[col] = append(cells[col], []int{col, row}) case 2: - if coordinates, err = areaRefToCoordinates(ref); err != nil { + if coordinates, err = rangeRefToCoordinates(ref); err != nil { return } _ = sortCoordinates(coordinates) @@ -691,34 +692,16 @@ func (f *File) addSheetNameSpace(sheet string, ns xml.Attr) { // isNumeric determines whether an expression is a valid numeric type and get // the precision for the numeric. -func isNumeric(s string) (bool, int) { - dot, e, n, p := false, false, false, 0 - for i, v := range s { - if v == '.' { - if dot { - return false, 0 - } - dot = true - } else if v == 'E' || v == 'e' { - e = true - } else if v < '0' || v > '9' { - if i == 0 && v == '-' { - continue - } - if e && v == '-' { - return true, 0 - } - if e && v == '+' { - p = 15 - continue - } - return false, 0 - } else { - p++ - } - n = true +func isNumeric(s string) (bool, int, float64) { + var decimal big.Float + _, ok := decimal.SetString(s) + if !ok { + return false, 0, 0 } - return n, p + var noScientificNotation string + flt, _ := decimal.Float64() + noScientificNotation = strconv.FormatFloat(flt, 'f', -1, 64) + return true, len(strings.ReplaceAll(noScientificNotation, ".", "")), flt } var ( diff --git a/lib_test.go b/lib_test.go index c42914d8aa..e96704f67c 100644 --- a/lib_test.go +++ b/lib_test.go @@ -217,15 +217,15 @@ func TestCoordinatesToCellName_Error(t *testing.T) { } } -func TestCoordinatesToAreaRef(t *testing.T) { +func TestCoordinatesToRangeRef(t *testing.T) { f := NewFile() - _, err := f.coordinatesToAreaRef([]int{}) + _, err := f.coordinatesToRangeRef([]int{}) assert.EqualError(t, err, ErrCoordinates.Error()) - _, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1}) + _, err = f.coordinatesToRangeRef([]int{1, -1, 1, 1}) assert.EqualError(t, err, "invalid cell reference [1, -1]") - _, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1}) + _, err = f.coordinatesToRangeRef([]int{1, 1, 1, -1}) assert.EqualError(t, err, "invalid cell reference [1, -1]") - ref, err := f.coordinatesToAreaRef([]int{1, 1, 1, 1}) + ref, err := f.coordinatesToRangeRef([]int{1, 1, 1, 1}) assert.NoError(t, err) assert.EqualValues(t, ref, "A1:A1") } diff --git a/merge.go b/merge.go index ac7fb0478e..04dc493d70 100644 --- a/merge.go +++ b/merge.go @@ -17,7 +17,7 @@ import "strings" func (mc *xlsxMergeCell) Rect() ([]int, error) { var err error if mc.rect == nil { - mc.rect, err = areaRefToCoordinates(mc.Ref) + mc.rect, err = rangeRefToCoordinates(mc.Ref) } return mc.rect, err } @@ -46,7 +46,7 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) { // |A8(x3,y4) C8(x4,y4)| // +------------------------+ func (f *File) MergeCell(sheet, hCell, vCell string) error { - rect, err := areaRefToCoordinates(hCell + ":" + vCell) + rect, err := rangeRefToCoordinates(hCell + ":" + vCell) if err != nil { return err } @@ -73,11 +73,11 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error { } // UnmergeCell provides a function to unmerge a given range reference. -// For example unmerge area D3:E9 on Sheet1: +// For example unmerge range reference D3:E9 on Sheet1: // // err := f.UnmergeCell("Sheet1", "D3", "E9") // -// Attention: overlapped areas will also be unmerged. +// Attention: overlapped range will also be unmerged. func (f *File) UnmergeCell(sheet, hCell, vCell string) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -85,7 +85,7 @@ func (f *File) UnmergeCell(sheet, hCell, vCell string) error { } ws.Lock() defer ws.Unlock() - rect1, err := areaRefToCoordinates(hCell + ":" + vCell) + rect1, err := rangeRefToCoordinates(hCell + ":" + vCell) if err != nil { return err } @@ -105,7 +105,7 @@ func (f *File) UnmergeCell(sheet, hCell, vCell string) error { if mergeCell == nil { continue } - rect2, _ := areaRefToCoordinates(mergeCell.Ref) + rect2, _ := rangeRefToCoordinates(mergeCell.Ref) if isOverlap(rect1, rect2) { continue } diff --git a/merge_test.go b/merge_test.go index f4d5f7ed8a..6977c5ac5c 100644 --- a/merge_test.go +++ b/merge_test.go @@ -169,7 +169,7 @@ func TestUnmergeCell(t *testing.T) { f = NewFile() assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) - // Test unmerged area on not exists worksheet. + // Test unmerged range reference on not exists worksheet. assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN does not exist") ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") diff --git a/numfmt.go b/numfmt.go index fd99240b80..09e64c96d3 100644 --- a/numfmt.go +++ b/numfmt.go @@ -279,7 +279,7 @@ var ( // prepareNumberic split the number into two before and after parts by a // decimal point. func (nf *numberFormat) prepareNumberic(value string) { - if nf.isNumeric, _ = isNumeric(value); !nf.isNumeric { + if nf.isNumeric, _, _ = isNumeric(value); !nf.isNumeric { return } } @@ -338,13 +338,13 @@ func (nf *numberFormat) positiveHandler() (result string) { continue } if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) { - if isNum, precision := isNumeric(nf.value); isNum { + if isNum, precision, decimal := isNumeric(nf.value); isNum { if nf.number < 1 { nf.result += "0" continue } if precision > 15 { - nf.result += roundPrecision(nf.value, 15) + nf.result += strconv.FormatFloat(decimal, 'f', -1, 64) } else { nf.result += fmt.Sprintf("%.f", nf.number) } @@ -902,13 +902,13 @@ func (nf *numberFormat) negativeHandler() (result string) { continue } if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) { - if isNum, precision := isNumeric(nf.value); isNum { + if isNum, precision, decimal := isNumeric(nf.value); isNum { if math.Abs(nf.number) < 1 { nf.result += "0" continue } if precision > 15 { - nf.result += strings.TrimLeft(roundPrecision(nf.value, 15), "-") + nf.result += strings.TrimLeft(strconv.FormatFloat(decimal, 'f', -1, 64), "-") } else { nf.result += fmt.Sprintf("%.f", math.Abs(nf.number)) } @@ -941,7 +941,7 @@ func (nf *numberFormat) textHandler() (result string) { // getValueSectionType returns its applicable number format expression section // based on the given value. func (nf *numberFormat) getValueSectionType(value string) (float64, string) { - isNum, _ := isNumeric(value) + isNum, _, _ := isNumeric(value) if !isNum { return 0, nfp.TokenSectionText } diff --git a/picture.go b/picture.go index 3b6d821380..e1c62f2e52 100644 --- a/picture.go +++ b/picture.go @@ -652,11 +652,11 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, formatSe if inMergeCell { continue } - if inMergeCell, err = f.checkCellInArea(cell, mergeCell[0]); err != nil { + if inMergeCell, err = f.checkCellInRangeRef(cell, mergeCell[0]); err != nil { return } if inMergeCell { - rng, _ = areaRangeToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis()) + rng, _ = cellRefsToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis()) _ = sortCoordinates(rng) } } diff --git a/pivotTable.go b/pivotTable.go index af30a0b722..8e16e0689a 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -81,8 +81,9 @@ type PivotTableField struct { // options. Note that the same fields can not in Columns, Rows and Filter // fields at the same time. // -// For example, create a pivot table on the Sheet1!$G$2:$M$34 area with the -// region Sheet1!$A$1:$E$31 as the data source, summarize by sum for sales: +// For example, create a pivot table on the Sheet1!$G$2:$M$34 range reference +// with the region Sheet1!$A$1:$E$31 as the data source, summarize by sum for +// sales: // // package main // @@ -205,7 +206,7 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { return "", []int{}, ErrParameterInvalid } trimRng := strings.ReplaceAll(rng[1], "$", "") - coordinates, err := areaRefToCoordinates(trimRng) + coordinates, err := rangeRefToCoordinates(trimRng) if err != nil { return rng[0], []int{}, err } diff --git a/rows.go b/rows.go index 561f64b243..2960aa461b 100644 --- a/rows.go +++ b/rows.go @@ -19,7 +19,6 @@ import ( "io/ioutil" "log" "math" - "math/big" "os" "strconv" @@ -486,32 +485,17 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { } return f.formattedValue(c.S, c.V, raw), nil default: - if isNum, precision := isNumeric(c.V); isNum && !raw { - if precision == 0 { - c.V = roundPrecision(c.V, 15) + if isNum, precision, decimal := isNumeric(c.V); isNum && !raw { + if precision > 15 { + c.V = strconv.FormatFloat(decimal, 'G', 15, 64) } else { - c.V = roundPrecision(c.V, -1) + c.V = strconv.FormatFloat(decimal, 'f', -1, 64) } } return f.formattedValue(c.S, c.V, raw), nil } } -// roundPrecision provides a function to format floating-point number text -// with precision, if the given text couldn't be parsed to float, this will -// return the original string. -func roundPrecision(text string, prec int) string { - decimal := big.Float{} - if _, ok := decimal.SetString(text); ok { - flt, _ := decimal.Float64() - if prec == -1 { - return strconv.FormatFloat(flt, 'G', 15, 64) - } - return strconv.FormatFloat(flt, 'f', -1, 64) - } - return text -} - // SetRowVisible provides a function to set visible of a single row by given // worksheet name and Excel row number. For example, hide row 2 in Sheet1: // @@ -732,7 +716,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in row++ } for _, rng := range ws.MergeCells.Cells { - coordinates, err := areaRefToCoordinates(rng.Ref) + coordinates, err := rangeRefToCoordinates(rng.Ref) if err != nil { return err } @@ -741,8 +725,8 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in } } for i := 0; i < len(ws.MergeCells.Cells); i++ { - areaData := ws.MergeCells.Cells[i] - coordinates, _ := areaRefToCoordinates(areaData.Ref) + mergedCells := ws.MergeCells.Cells[i] + coordinates, _ := rangeRefToCoordinates(mergedCells.Ref) x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] if y1 == y2 && y1 == row { from, _ := CoordinatesToCellName(x1, row2) diff --git a/rows_test.go b/rows_test.go index d51e256857..8ce007ff85 100644 --- a/rows_test.go +++ b/rows_test.go @@ -995,10 +995,6 @@ func TestNumberFormats(t *testing.T) { assert.NoError(t, f.Close()) } -func TestRoundPrecision(t *testing.T) { - assert.Equal(t, "text", roundPrecision("text", 0)) -} - func BenchmarkRows(b *testing.B) { f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) for i := 0; i < b.N; i++ { diff --git a/sheet.go b/sheet.go index ee4f667553..fe24b18c90 100644 --- a/sheet.go +++ b/sheet.go @@ -39,6 +39,9 @@ import ( // `Sheet1` will be created. func (f *File) NewSheet(sheet string) int { // Check if the worksheet already exists + if trimSheetName(sheet) == "" { + return -1 + } index := f.GetSheetIndex(sheet) if index != -1 { return index diff --git a/sheet_test.go b/sheet_test.go index 324d626bfd..87c36d469f 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -90,6 +90,8 @@ func TestNewSheet(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewSheet.xlsx"))) // create new worksheet with already exists name assert.Equal(t, f.GetSheetIndex("Sheet2"), f.NewSheet("Sheet2")) + // create new worksheet with empty sheet name + assert.Equal(t, -1, f.NewSheet(":\\/?*[]")) } func TestSetPane(t *testing.T) { diff --git a/stream.go b/stream.go index 84299cbf31..019731f92c 100644 --- a/stream.go +++ b/stream.go @@ -145,7 +145,7 @@ func (sw *StreamWriter) AddTable(hCell, vCell, format string) error { return err } - coordinates, err := areaRangeToCoordinates(hCell, vCell) + coordinates, err := cellRefsToCoordinates(hCell, vCell) if err != nil { return err } @@ -157,7 +157,7 @@ func (sw *StreamWriter) AddTable(hCell, vCell, format string) error { } // Correct table reference range, such correct C1:B3 to B1:C3. - ref, err := sw.File.coordinatesToAreaRef(coordinates) + ref, err := sw.File.coordinatesToRangeRef(coordinates) if err != nil { return err } @@ -412,7 +412,7 @@ func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { // the StreamWriter. Don't create a merged cell that overlaps with another // existing merged cell. func (sw *StreamWriter) MergeCell(hCell, vCell string) error { - _, err := areaRangeToCoordinates(hCell, vCell) + _, err := cellRefsToCoordinates(hCell, vCell) if err != nil { return err } diff --git a/styles.go b/styles.go index 587fd749a5..a4f5dc48dc 100644 --- a/styles.go +++ b/styles.go @@ -2486,11 +2486,14 @@ func (f *File) GetCellStyle(sheet, cell string) (int, error) { if err != nil { return 0, err } - c, col, row, err := f.prepareCell(ws, cell) + col, row, err := CellNameToCoordinates(cell) if err != nil { return 0, err } - return f.prepareCellStyle(ws, col, row, c.S), err + prepareSheetXML(ws, col, row) + ws.Lock() + defer ws.Unlock() + return f.prepareCellStyle(ws, col, row, ws.SheetData.Row[row-1].C[col-1].S), err } // SetCellStyle provides a function to add style attribute for cells by given @@ -2856,7 +2859,7 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // max_color - Same as min_color, see above. // // bar_color - Used for data_bar. Same as min_color, see above. -func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { +func (f *File) SetConditionalFormat(sheet, reference, formatSet string) error { var format []*formatConditional err := json.Unmarshal([]byte(formatSet), &format) if err != nil { @@ -2897,7 +2900,7 @@ func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { } ws.ConditionalFormatting = append(ws.ConditionalFormatting, &xlsxConditionalFormatting{ - SQRef: area, + SQRef: reference, CfRule: cfRule, }) return err diff --git a/table.go b/table.go index 66ba72972a..7ef75625b3 100644 --- a/table.go +++ b/table.go @@ -48,7 +48,7 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) { // Note that the table must be at least two lines including the header. The // header cells must contain strings and must be unique, and must set the // header row data of the table before calling the AddTable function. Multiple -// tables coordinate areas that can't have an intersection. +// tables range reference that can't have an intersection. // // table_name: The name of the table, in the same worksheet name of the table should be unique // @@ -167,7 +167,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet } // Correct table range reference, such correct C1:B3 to B1:C3. - ref, err := f.coordinatesToAreaRef([]int{x1, y1, x2, y2}) + ref, err := f.coordinatesToRangeRef([]int{x1, y1, x2, y2}) if err != nil { return err } From 53a495563a2b9acf09ae45eae05d5f33aa242a87 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 29 Sep 2022 22:00:21 +0800 Subject: [PATCH 109/213] This closes #1358, made a refactor with breaking changes, see details: This made a refactor with breaking changes: Motivation and Context When I decided to add set horizontal centered support for this library to resolve #1358, the reason I made this huge breaking change was: - There are too many exported types for set sheet view, properties, and format properties, although a function using the functional options pattern can be optimized by returning an anonymous function, these types or property set or get function has no binding categorization, so I change these functions like `SetAppProps` to accept a pointer of options structure. - Users can not easily find out which properties should be in the `SetSheetPrOptions` or `SetSheetFormatPr` categories - Nested properties cannot proceed modify easily Introduce 5 new export data types: `HeaderFooterOptions`, `PageLayoutMarginsOptions`, `PageLayoutOptions`, `SheetPropsOptions`, and `ViewOptions` Rename 4 exported data types: - Rename `PivotTableOption` to `PivotTableOptions` - Rename `FormatHeaderFooter` to `HeaderFooterOptions` - Rename `FormatSheetProtection` to `SheetProtectionOptions` - Rename `SparklineOption` to `SparklineOptions` Remove 54 exported types: `AutoPageBreaks`, `BaseColWidth`, `BlackAndWhite`, `CodeName`, `CustomHeight`, `Date1904`, `DefaultColWidth`, `DefaultGridColor`, `DefaultRowHeight`, `EnableFormatConditionsCalculation`, `FilterPrivacy`, `FirstPageNumber`, `FitToHeight`, `FitToPage`, `FitToWidth`, `OutlineSummaryBelow`, `PageLayoutOption`, `PageLayoutOptionPtr`, `PageLayoutOrientation`, `PageLayoutPaperSize`, `PageLayoutScale`, `PageMarginBottom`, `PageMarginFooter`, `PageMarginHeader`, `PageMarginLeft`, `PageMarginRight`, `PageMarginsOptions`, `PageMarginsOptionsPtr`, `PageMarginTop`, `Published`, `RightToLeft`, `SheetFormatPrOptions`, `SheetFormatPrOptionsPtr`, `SheetPrOption`, `SheetPrOptionPtr`, `SheetViewOption`, `SheetViewOptionPtr`, `ShowFormulas`, `ShowGridLines`, `ShowRowColHeaders`, `ShowRuler`, `ShowZeros`, `TabColorIndexed`, `TabColorRGB`, `TabColorTheme`, `TabColorTint`, `ThickBottom`, `ThickTop`, `TopLeftCell`, `View`, `WorkbookPrOption`, `WorkbookPrOptionPtr`, `ZeroHeight` and `ZoomScale` Remove 2 exported constants: `OrientationPortrait` and `OrientationLandscape` Change 8 functions: - Change the `func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error` to `func (f *File) SetPageLayout(sheet string, opts *PageLayoutOptions) error` - Change the `func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error` to `func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error)` - Change the `func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error` to `func (f *File) SetPageMargins(sheet string, opts *PageLayoutMarginsOptions) error` - Change the `func (f *File) GetPageMargins(sheet string, opts ...PageMarginsOptionsPtr) error` to `func (f *File) GetPageMargins(sheet string) (PageLayoutMarginsOptions, error)` - Change the `func (f *File) SetSheetViewOptions(sheet string, viewIndex int, opts ...SheetViewOption) error` to `func (f *File) SetSheetView(sheet string, viewIndex int, opts *ViewOptions) error` - Change the `func (f *File) GetSheetViewOptions(sheet string, viewIndex int, opts ...SheetViewOptionPtr) error` to `func (f *File) GetSheetView(sheet string, viewIndex int) (ViewOptions, error)` - Change the `func (f *File) SetWorkbookPrOptions(opts ...WorkbookPrOption) error` to `func (f *File) SetWorkbookProps(opts *WorkbookPropsOptions) error` - Change the `func (f *File) GetWorkbookPrOptions(opts ...WorkbookPrOptionPtr) error` to `func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error)` Introduce new function to instead of existing functions: - New function `func (f *File) SetSheetProps(sheet string, opts *SheetPropsOptions) error` instead of `func (f *File) SetSheetPrOptions(sheet string, opts ...SheetPrOption) error` and `func (f *File) SetSheetFormatPr(sheet string, opts ...SheetFormatPrOption --- chart.go | 64 ++-- comment.go | 41 +-- docProps.go | 28 +- drawing.go | 347 +++++++++---------- excelize_test.go | 26 +- lib.go | 19 +- picture.go | 60 ++-- pivotTable.go | 32 +- pivotTable_test.go | 58 ++-- rows_test.go | 8 +- shape.go | 58 ++-- sheet.go | 324 ++++++------------ sheet_test.go | 159 ++------- sheetpr.go | 808 ++++++++++++--------------------------------- sheetpr_test.go | 550 +++++------------------------- sheetview.go | 277 +++++----------- sheetview_test.go | 242 +++----------- sparkline.go | 34 +- sparkline_test.go | 71 ++-- stream.go | 16 +- stream_test.go | 2 +- styles.go | 60 ++-- styles_test.go | 4 +- table.go | 60 ++-- table_test.go | 10 +- workbook.go | 125 ++----- workbook_test.go | 68 +--- xmlChart.go | 73 ++-- xmlComments.go | 4 +- xmlDrawing.go | 36 +- xmlTable.go | 8 +- xmlWorkbook.go | 15 +- xmlWorksheet.go | 278 +++++++++++----- 33 files changed, 1343 insertions(+), 2622 deletions(-) diff --git a/chart.go b/chart.go index 267e0ddc1e..24ad2076eb 100644 --- a/chart.go +++ b/chart.go @@ -469,30 +469,30 @@ var ( } ) -// parseFormatChartSet provides a function to parse the format settings of the +// parseChartOptions provides a function to parse the format settings of the // chart with default value. -func parseFormatChartSet(formatSet string) (*formatChart, error) { - format := formatChart{ - Dimension: formatChartDimension{ +func parseChartOptions(opts string) (*chartOptions, error) { + options := chartOptions{ + Dimension: chartDimensionOptions{ Width: 480, Height: 290, }, - Format: formatPicture{ + Format: pictureOptions{ FPrintsWithSheet: true, XScale: 1, YScale: 1, }, - Legend: formatChartLegend{ + Legend: chartLegendOptions{ Position: "bottom", }, - Title: formatChartTitle{ + Title: chartTitleOptions{ Name: " ", }, VaryColors: true, ShowBlanksAs: "gap", } - err := json.Unmarshal([]byte(formatSet), &format) - return &format, err + err := json.Unmarshal([]byte(opts), &options) + return &options, err } // AddChart provides the method to add chart in a sheet by given chart format @@ -881,13 +881,13 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // fmt.Println(err) // } // } -func (f *File) AddChart(sheet, cell, format string, combo ...string) error { +func (f *File) AddChart(sheet, cell, opts string, combo ...string) error { // Read sheet data. ws, err := f.workSheetReader(sheet) if err != nil { return err } - formatSet, comboCharts, err := f.getFormatChart(format, combo) + options, comboCharts, err := f.getChartOptions(opts, combo) if err != nil { return err } @@ -898,11 +898,11 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML) drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") - err = f.addDrawingChart(sheet, drawingXML, cell, formatSet.Dimension.Width, formatSet.Dimension.Height, drawingRID, &formatSet.Format) + err = f.addDrawingChart(sheet, drawingXML, cell, options.Dimension.Width, options.Dimension.Height, drawingRID, &options.Format) if err != nil { return err } - f.addChart(formatSet, comboCharts) + f.addChart(options, comboCharts) f.addContentTypePart(chartID, "chart") f.addContentTypePart(drawingID, "drawings") f.addSheetNameSpace(sheet, SourceRelationship) @@ -913,12 +913,12 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { // format set (such as offset, scale, aspect ratio setting and print settings) // and properties set. In Excel a chartsheet is a worksheet that only contains // a chart. -func (f *File) AddChartSheet(sheet, format string, combo ...string) error { +func (f *File) AddChartSheet(sheet, opts string, combo ...string) error { // Check if the worksheet already exists if f.GetSheetIndex(sheet) != -1 { return ErrExistsWorksheet } - formatSet, comboCharts, err := f.getFormatChart(format, combo) + options, comboCharts, err := f.getChartOptions(opts, combo) if err != nil { return err } @@ -945,8 +945,8 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { f.prepareChartSheetDrawing(&cs, drawingID, sheet) drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") - f.addSheetDrawingChart(drawingXML, drawingRID, &formatSet.Format) - f.addChart(formatSet, comboCharts) + f.addSheetDrawingChart(drawingXML, drawingRID, &options.Format) + f.addChart(options, comboCharts) f.addContentTypePart(chartID, "chart") f.addContentTypePart(sheetID, "chartsheet") f.addContentTypePart(drawingID, "drawings") @@ -960,45 +960,45 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { return err } -// getFormatChart provides a function to check format set of the chart and +// getChartOptions provides a function to check format set of the chart and // create chart format. -func (f *File) getFormatChart(format string, combo []string) (*formatChart, []*formatChart, error) { - var comboCharts []*formatChart - formatSet, err := parseFormatChartSet(format) +func (f *File) getChartOptions(opts string, combo []string) (*chartOptions, []*chartOptions, error) { + var comboCharts []*chartOptions + options, err := parseChartOptions(opts) if err != nil { - return formatSet, comboCharts, err + return options, comboCharts, err } for _, comboFormat := range combo { - comboChart, err := parseFormatChartSet(comboFormat) + comboChart, err := parseChartOptions(comboFormat) if err != nil { - return formatSet, comboCharts, err + return options, comboCharts, err } if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { - return formatSet, comboCharts, newUnsupportedChartType(comboChart.Type) + return options, comboCharts, newUnsupportedChartType(comboChart.Type) } comboCharts = append(comboCharts, comboChart) } - if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { - return formatSet, comboCharts, newUnsupportedChartType(formatSet.Type) + if _, ok := chartValAxNumFmtFormatCode[options.Type]; !ok { + return options, comboCharts, newUnsupportedChartType(options.Type) } - return formatSet, comboCharts, err + return options, comboCharts, err } // DeleteChart provides a function to delete chart in spreadsheet by given // worksheet name and cell reference. -func (f *File) DeleteChart(sheet, cell string) (err error) { +func (f *File) DeleteChart(sheet, cell string) error { col, row, err := CellNameToCoordinates(cell) if err != nil { - return + return err } col-- row-- ws, err := f.workSheetReader(sheet) if err != nil { - return + return err } if ws.Drawing == nil { - return + return err } drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl") return f.deleteDrawing(col, row, drawingXML, "Chart") diff --git a/comment.go b/comment.go index 41f91bb2bf..3d0832469e 100644 --- a/comment.go +++ b/comment.go @@ -23,15 +23,15 @@ import ( "strings" ) -// parseFormatCommentsSet provides a function to parse the format settings of +// parseCommentOptions provides a function to parse the format settings of // the comment with default value. -func parseFormatCommentsSet(formatSet string) (*formatComment, error) { - format := formatComment{ +func parseCommentOptions(opts string) (*commentOptions, error) { + options := commentOptions{ Author: "Author:", Text: " ", } - err := json.Unmarshal([]byte(formatSet), &format) - return &format, err + err := json.Unmarshal([]byte(opts), &options) + return &options, err } // GetComments retrieves all comments and returns a map of worksheet name to @@ -93,8 +93,8 @@ func (f *File) getSheetComments(sheetFile string) string { // comment in Sheet1!$A$30: // // err := f.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`) -func (f *File) AddComment(sheet, cell, format string) error { - formatSet, err := parseFormatCommentsSet(format) +func (f *File) AddComment(sheet, cell, opts string) error { + options, err := parseCommentOptions(opts) if err != nil { return err } @@ -123,19 +123,19 @@ func (f *File) AddComment(sheet, cell, format string) error { } commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml" var colCount int - for i, l := range strings.Split(formatSet.Text, "\n") { + for i, l := range strings.Split(options.Text, "\n") { if ll := len(l); ll > colCount { if i == 0 { - ll += len(formatSet.Author) + ll += len(options.Author) } colCount = ll } } - err = f.addDrawingVML(commentID, drawingVML, cell, strings.Count(formatSet.Text, "\n")+1, colCount) + err = f.addDrawingVML(commentID, drawingVML, cell, strings.Count(options.Text, "\n")+1, colCount) if err != nil { return err } - f.addComment(commentsXML, cell, formatSet) + f.addComment(commentsXML, cell, options) f.addContentTypePart(commentID, "comments") return err } @@ -144,11 +144,12 @@ func (f *File) AddComment(sheet, cell, format string) error { // worksheet name. For example, delete the comment in Sheet1!$A$30: // // err := f.DeleteComment("Sheet1", "A30") -func (f *File) DeleteComment(sheet, cell string) (err error) { +func (f *File) DeleteComment(sheet, cell string) error { + var err error sheetXMLPath, ok := f.getSheetXMLPath(sheet) if !ok { err = newNoExistSheetError(sheet) - return + return err } commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath)) if !strings.HasPrefix(commentsXML, "/") { @@ -173,7 +174,7 @@ func (f *File) DeleteComment(sheet, cell string) (err error) { } f.Comments[commentsXML] = comments } - return + return err } // addDrawingVML provides a function to create comment as @@ -279,9 +280,9 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, // addComment provides a function to create chart as xl/comments%d.xml by // given cell and format sets. -func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { - a := formatSet.Author - t := formatSet.Text +func (f *File) addComment(commentsXML, cell string, opts *commentOptions) { + a := opts.Author + t := opts.Text if len(a) > MaxFieldLength { a = a[:MaxFieldLength] } @@ -291,10 +292,10 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { comments := f.commentsReader(commentsXML) authorID := 0 if comments == nil { - comments = &xlsxComments{Authors: xlsxAuthor{Author: []string{formatSet.Author}}} + comments = &xlsxComments{Authors: xlsxAuthor{Author: []string{opts.Author}}} } - if inStrSlice(comments.Authors.Author, formatSet.Author, true) == -1 { - comments.Authors.Author = append(comments.Authors.Author, formatSet.Author) + if inStrSlice(comments.Authors.Author, opts.Author, true) == -1 { + comments.Authors.Author = append(comments.Authors.Author, opts.Author) authorID = len(comments.Authors.Author) - 1 } defaultFont := f.GetDefaultFont() diff --git a/docProps.go b/docProps.go index 00ff808bbd..4ee46ad1d0 100644 --- a/docProps.go +++ b/docProps.go @@ -64,19 +64,20 @@ import ( // HyperlinksChanged: true, // AppVersion: "16.0000", // }) -func (f *File) SetAppProps(appProperties *AppProperties) (err error) { +func (f *File) SetAppProps(appProperties *AppProperties) error { var ( app *xlsxProperties + err error + field string fields []string - output []byte immutable, mutable reflect.Value - field string + output []byte ) app = new(xlsxProperties) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsApp)))). Decode(app); err != nil && err != io.EOF { err = newDecodeXMLError(err) - return + return err } fields = []string{"Application", "ScaleCrop", "DocSecurity", "Company", "LinksUpToDate", "HyperlinksChanged", "AppVersion"} immutable, mutable = reflect.ValueOf(*appProperties), reflect.ValueOf(app).Elem() @@ -94,7 +95,7 @@ func (f *File) SetAppProps(appProperties *AppProperties) (err error) { app.Vt = NameSpaceDocumentPropertiesVariantTypes.Value output, err = xml.Marshal(app) f.saveFileList(defaultXMLPathDocPropsApp, output) - return + return err } // GetAppProps provides a function to get document application properties. @@ -167,23 +168,24 @@ func (f *File) GetAppProps() (ret *AppProperties, err error) { // Language: "en-US", // Version: "1.0.0", // }) -func (f *File) SetDocProps(docProperties *DocProperties) (err error) { +func (f *File) SetDocProps(docProperties *DocProperties) error { var ( core *decodeCoreProperties - newProps *xlsxCoreProperties + err error + field, val string fields []string - output []byte immutable, mutable reflect.Value - field, val string + newProps *xlsxCoreProperties + output []byte ) core = new(decodeCoreProperties) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))). Decode(core); err != nil && err != io.EOF { err = newDecodeXMLError(err) - return + return err } - newProps, err = &xlsxCoreProperties{ + newProps = &xlsxCoreProperties{ Dc: NameSpaceDublinCore, Dcterms: NameSpaceDublinCoreTerms, Dcmitype: NameSpaceDublinCoreMetadataInitiative, @@ -200,7 +202,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { ContentStatus: core.ContentStatus, Category: core.Category, Version: core.Version, - }, nil + } if core.Created != nil { newProps.Created = &xlsxDcTerms{Type: core.Created.Type, Text: core.Created.Text} } @@ -226,7 +228,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { output, err = xml.Marshal(newProps) f.saveFileList(defaultXMLPathDocPropsCore, output) - return + return err } // GetDocProps provides a function to get document core properties. diff --git a/drawing.go b/drawing.go index e05c9beb8f..3ef58212b3 100644 --- a/drawing.go +++ b/drawing.go @@ -56,7 +56,7 @@ func (f *File) prepareChartSheetDrawing(cs *xlsxChartsheet, drawingID int, sheet // addChart provides a function to create chart as xl/charts/chart%d.xml by // given format sets. -func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { +func (f *File) addChart(opts *chartOptions, comboCharts []*chartOptions) { count := f.countCharts() xlsxChartSpace := xlsxChartSpace{ XMLNSa: NameSpaceDrawingML.Value, @@ -101,7 +101,7 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { Lang: "en-US", AltLang: "en-US", }, - T: formatSet.Title.Name, + T: opts.Title.Name, }, }, }, @@ -124,10 +124,10 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { Overlay: &attrValBool{Val: boolPtr(false)}, }, View3D: &cView3D{ - RotX: &attrValInt{Val: intPtr(chartView3DRotX[formatSet.Type])}, - RotY: &attrValInt{Val: intPtr(chartView3DRotY[formatSet.Type])}, - Perspective: &attrValInt{Val: intPtr(chartView3DPerspective[formatSet.Type])}, - RAngAx: &attrValInt{Val: intPtr(chartView3DRAngAx[formatSet.Type])}, + RotX: &attrValInt{Val: intPtr(chartView3DRotX[opts.Type])}, + RotY: &attrValInt{Val: intPtr(chartView3DRotY[opts.Type])}, + Perspective: &attrValInt{Val: intPtr(chartView3DPerspective[opts.Type])}, + RAngAx: &attrValInt{Val: intPtr(chartView3DRAngAx[opts.Type])}, }, Floor: &cThicknessSpPr{ Thickness: &attrValInt{Val: intPtr(0)}, @@ -140,12 +140,12 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { }, PlotArea: &cPlotArea{}, Legend: &cLegend{ - LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[formatSet.Legend.Position])}, + LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[opts.Legend.Position])}, Overlay: &attrValBool{Val: boolPtr(false)}, }, PlotVisOnly: &attrValBool{Val: boolPtr(false)}, - DispBlanksAs: &attrValString{Val: stringPtr(formatSet.ShowBlanksAs)}, + DispBlanksAs: &attrValString{Val: stringPtr(opts.ShowBlanksAs)}, ShowDLblsOverMax: &attrValBool{Val: boolPtr(false)}, }, SpPr: &cSpPr{ @@ -181,7 +181,7 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { }, }, } - plotAreaFunc := map[string]func(*formatChart) *cPlotArea{ + plotAreaFunc := map[string]func(*chartOptions) *cPlotArea{ Area: f.drawBaseChart, AreaStacked: f.drawBaseChart, AreaPercentStacked: f.drawBaseChart, @@ -237,7 +237,7 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { Bubble: f.drawBaseChart, Bubble3D: f.drawBaseChart, } - if formatSet.Legend.None { + if opts.Legend.None { xlsxChartSpace.Chart.Legend = nil } addChart := func(c, p *cPlotArea) { @@ -250,8 +250,8 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { immutable.FieldByName(mutable.Type().Field(i).Name).Set(field) } } - addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Type](formatSet)) - order := len(formatSet.Series) + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[opts.Type](opts)) + order := len(opts.Series) for idx := range comboCharts { comboCharts[idx].order = order addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](comboCharts[idx])) @@ -264,7 +264,7 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { // drawBaseChart provides a function to draw the c:plotArea element for bar, // and column series charts by given format sets. -func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { +func (f *File) drawBaseChart(opts *chartOptions) *cPlotArea { c := cCharts{ BarDir: &attrValString{ Val: stringPtr("col"), @@ -273,11 +273,11 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { Val: stringPtr("clustered"), }, VaryColors: &attrValBool{ - Val: boolPtr(formatSet.VaryColors), + Val: boolPtr(opts.VaryColors), }, - Ser: f.drawChartSeries(formatSet), - Shape: f.drawChartShape(formatSet), - DLbls: f.drawChartDLbls(formatSet), + Ser: f.drawChartSeries(opts), + Shape: f.drawChartShape(opts), + DLbls: f.drawChartDLbls(opts), AxID: []*attrValInt{ {Val: intPtr(754001152)}, {Val: intPtr(753999904)}, @@ -285,17 +285,17 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { Overlap: &attrValInt{Val: intPtr(100)}, } var ok bool - if *c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { + if *c.BarDir.Val, ok = plotAreaChartBarDir[opts.Type]; !ok { c.BarDir = nil } - if *c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { + if *c.Grouping.Val, ok = plotAreaChartGrouping[opts.Type]; !ok { c.Grouping = nil } - if *c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok { + if *c.Overlap.Val, ok = plotAreaChartOverlap[opts.Type]; !ok { c.Overlap = nil } - catAx := f.drawPlotAreaCatAx(formatSet) - valAx := f.drawPlotAreaValAx(formatSet) + catAx := f.drawPlotAreaCatAx(opts) + valAx := f.drawPlotAreaValAx(opts) charts := map[string]*cPlotArea{ "area": { AreaChart: &c, @@ -508,23 +508,23 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { ValAx: valAx, }, } - return charts[formatSet.Type] + return charts[opts.Type] } // drawDoughnutChart provides a function to draw the c:plotArea element for // doughnut chart by given format sets. -func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea { +func (f *File) drawDoughnutChart(opts *chartOptions) *cPlotArea { holeSize := 75 - if formatSet.HoleSize > 0 && formatSet.HoleSize <= 90 { - holeSize = formatSet.HoleSize + if opts.HoleSize > 0 && opts.HoleSize <= 90 { + holeSize = opts.HoleSize } return &cPlotArea{ DoughnutChart: &cCharts{ VaryColors: &attrValBool{ - Val: boolPtr(formatSet.VaryColors), + Val: boolPtr(opts.VaryColors), }, - Ser: f.drawChartSeries(formatSet), + Ser: f.drawChartSeries(opts), HoleSize: &attrValInt{Val: intPtr(holeSize)}, }, } @@ -532,65 +532,65 @@ func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea { // drawLineChart provides a function to draw the c:plotArea element for line // chart by given format sets. -func (f *File) drawLineChart(formatSet *formatChart) *cPlotArea { +func (f *File) drawLineChart(opts *chartOptions) *cPlotArea { return &cPlotArea{ LineChart: &cCharts{ Grouping: &attrValString{ - Val: stringPtr(plotAreaChartGrouping[formatSet.Type]), + Val: stringPtr(plotAreaChartGrouping[opts.Type]), }, VaryColors: &attrValBool{ Val: boolPtr(false), }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), + Ser: f.drawChartSeries(opts), + DLbls: f.drawChartDLbls(opts), AxID: []*attrValInt{ {Val: intPtr(754001152)}, {Val: intPtr(753999904)}, }, }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), + CatAx: f.drawPlotAreaCatAx(opts), + ValAx: f.drawPlotAreaValAx(opts), } } // drawPieChart provides a function to draw the c:plotArea element for pie // chart by given format sets. -func (f *File) drawPieChart(formatSet *formatChart) *cPlotArea { +func (f *File) drawPieChart(opts *chartOptions) *cPlotArea { return &cPlotArea{ PieChart: &cCharts{ VaryColors: &attrValBool{ - Val: boolPtr(formatSet.VaryColors), + Val: boolPtr(opts.VaryColors), }, - Ser: f.drawChartSeries(formatSet), + Ser: f.drawChartSeries(opts), }, } } // drawPie3DChart provides a function to draw the c:plotArea element for 3D // pie chart by given format sets. -func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea { +func (f *File) drawPie3DChart(opts *chartOptions) *cPlotArea { return &cPlotArea{ Pie3DChart: &cCharts{ VaryColors: &attrValBool{ - Val: boolPtr(formatSet.VaryColors), + Val: boolPtr(opts.VaryColors), }, - Ser: f.drawChartSeries(formatSet), + Ser: f.drawChartSeries(opts), }, } } // drawPieOfPieChart provides a function to draw the c:plotArea element for // pie chart by given format sets. -func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { +func (f *File) drawPieOfPieChart(opts *chartOptions) *cPlotArea { return &cPlotArea{ OfPieChart: &cCharts{ OfPieType: &attrValString{ Val: stringPtr("pie"), }, VaryColors: &attrValBool{ - Val: boolPtr(formatSet.VaryColors), + Val: boolPtr(opts.VaryColors), }, - Ser: f.drawChartSeries(formatSet), + Ser: f.drawChartSeries(opts), SerLines: &attrValString{}, }, } @@ -598,16 +598,16 @@ func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { // drawBarOfPieChart provides a function to draw the c:plotArea element for // pie chart by given format sets. -func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea { +func (f *File) drawBarOfPieChart(opts *chartOptions) *cPlotArea { return &cPlotArea{ OfPieChart: &cCharts{ OfPieType: &attrValString{ Val: stringPtr("bar"), }, VaryColors: &attrValBool{ - Val: boolPtr(formatSet.VaryColors), + Val: boolPtr(opts.VaryColors), }, - Ser: f.drawChartSeries(formatSet), + Ser: f.drawChartSeries(opts), SerLines: &attrValString{}, }, } @@ -615,7 +615,7 @@ func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea { // drawRadarChart provides a function to draw the c:plotArea element for radar // chart by given format sets. -func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { +func (f *File) drawRadarChart(opts *chartOptions) *cPlotArea { return &cPlotArea{ RadarChart: &cCharts{ RadarStyle: &attrValString{ @@ -624,21 +624,21 @@ func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { VaryColors: &attrValBool{ Val: boolPtr(false), }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), + Ser: f.drawChartSeries(opts), + DLbls: f.drawChartDLbls(opts), AxID: []*attrValInt{ {Val: intPtr(754001152)}, {Val: intPtr(753999904)}, }, }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), + CatAx: f.drawPlotAreaCatAx(opts), + ValAx: f.drawPlotAreaValAx(opts), } } // drawScatterChart provides a function to draw the c:plotArea element for // scatter chart by given format sets. -func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { +func (f *File) drawScatterChart(opts *chartOptions) *cPlotArea { return &cPlotArea{ ScatterChart: &cCharts{ ScatterStyle: &attrValString{ @@ -647,35 +647,35 @@ func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { VaryColors: &attrValBool{ Val: boolPtr(false), }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), + Ser: f.drawChartSeries(opts), + DLbls: f.drawChartDLbls(opts), AxID: []*attrValInt{ {Val: intPtr(754001152)}, {Val: intPtr(753999904)}, }, }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), + CatAx: f.drawPlotAreaCatAx(opts), + ValAx: f.drawPlotAreaValAx(opts), } } // drawSurface3DChart provides a function to draw the c:surface3DChart element by // given format sets. -func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { +func (f *File) drawSurface3DChart(opts *chartOptions) *cPlotArea { plotArea := &cPlotArea{ Surface3DChart: &cCharts{ - Ser: f.drawChartSeries(formatSet), + Ser: f.drawChartSeries(opts), AxID: []*attrValInt{ {Val: intPtr(754001152)}, {Val: intPtr(753999904)}, {Val: intPtr(832256642)}, }, }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - SerAx: f.drawPlotAreaSerAx(formatSet), + CatAx: f.drawPlotAreaCatAx(opts), + ValAx: f.drawPlotAreaValAx(opts), + SerAx: f.drawPlotAreaSerAx(opts), } - if formatSet.Type == WireframeSurface3D { + if opts.Type == WireframeSurface3D { plotArea.Surface3DChart.Wireframe = &attrValBool{Val: boolPtr(true)} } return plotArea @@ -683,21 +683,21 @@ func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { // drawSurfaceChart provides a function to draw the c:surfaceChart element by // given format sets. -func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { +func (f *File) drawSurfaceChart(opts *chartOptions) *cPlotArea { plotArea := &cPlotArea{ SurfaceChart: &cCharts{ - Ser: f.drawChartSeries(formatSet), + Ser: f.drawChartSeries(opts), AxID: []*attrValInt{ {Val: intPtr(754001152)}, {Val: intPtr(753999904)}, {Val: intPtr(832256642)}, }, }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - SerAx: f.drawPlotAreaSerAx(formatSet), + CatAx: f.drawPlotAreaCatAx(opts), + ValAx: f.drawPlotAreaValAx(opts), + SerAx: f.drawPlotAreaSerAx(opts), } - if formatSet.Type == WireframeContour { + if opts.Type == WireframeContour { plotArea.SurfaceChart.Wireframe = &attrValBool{Val: boolPtr(true)} } return plotArea @@ -705,7 +705,7 @@ func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { // drawChartShape provides a function to draw the c:shape element by given // format sets. -func (f *File) drawChartShape(formatSet *formatChart) *attrValString { +func (f *File) drawChartShape(opts *chartOptions) *attrValString { shapes := map[string]string{ Bar3DConeClustered: "cone", Bar3DConeStacked: "cone", @@ -729,7 +729,7 @@ func (f *File) drawChartShape(formatSet *formatChart) *attrValString { Col3DCylinderStacked: "cylinder", Col3DCylinderPercentStacked: "cylinder", } - if shape, ok := shapes[formatSet.Type]; ok { + if shape, ok := shapes[opts.Type]; ok { return &attrValString{Val: stringPtr(shape)} } return nil @@ -737,29 +737,29 @@ func (f *File) drawChartShape(formatSet *formatChart) *attrValString { // drawChartSeries provides a function to draw the c:ser element by given // format sets. -func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { +func (f *File) drawChartSeries(opts *chartOptions) *[]cSer { var ser []cSer - for k := range formatSet.Series { + for k := range opts.Series { ser = append(ser, cSer{ - IDx: &attrValInt{Val: intPtr(k + formatSet.order)}, - Order: &attrValInt{Val: intPtr(k + formatSet.order)}, + IDx: &attrValInt{Val: intPtr(k + opts.order)}, + Order: &attrValInt{Val: intPtr(k + opts.order)}, Tx: &cTx{ StrRef: &cStrRef{ - F: formatSet.Series[k].Name, + F: opts.Series[k].Name, }, }, - SpPr: f.drawChartSeriesSpPr(k, formatSet), - Marker: f.drawChartSeriesMarker(k, formatSet), - DPt: f.drawChartSeriesDPt(k, formatSet), - DLbls: f.drawChartSeriesDLbls(formatSet), + SpPr: f.drawChartSeriesSpPr(k, opts), + Marker: f.drawChartSeriesMarker(k, opts), + DPt: f.drawChartSeriesDPt(k, opts), + DLbls: f.drawChartSeriesDLbls(opts), InvertIfNegative: &attrValBool{Val: boolPtr(false)}, - Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), - Smooth: &attrValBool{Val: boolPtr(formatSet.Series[k].Line.Smooth)}, - Val: f.drawChartSeriesVal(formatSet.Series[k], formatSet), - XVal: f.drawChartSeriesXVal(formatSet.Series[k], formatSet), - YVal: f.drawChartSeriesYVal(formatSet.Series[k], formatSet), - BubbleSize: f.drawCharSeriesBubbleSize(formatSet.Series[k], formatSet), - Bubble3D: f.drawCharSeriesBubble3D(formatSet), + Cat: f.drawChartSeriesCat(opts.Series[k], opts), + Smooth: &attrValBool{Val: boolPtr(opts.Series[k].Line.Smooth)}, + Val: f.drawChartSeriesVal(opts.Series[k], opts), + XVal: f.drawChartSeriesXVal(opts.Series[k], opts), + YVal: f.drawChartSeriesYVal(opts.Series[k], opts), + BubbleSize: f.drawCharSeriesBubbleSize(opts.Series[k], opts), + Bubble3D: f.drawCharSeriesBubble3D(opts), }) } return &ser @@ -767,15 +767,15 @@ func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { // drawChartSeriesSpPr provides a function to draw the c:spPr element by given // format sets. -func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { +func (f *File) drawChartSeriesSpPr(i int, opts *chartOptions) *cSpPr { var srgbClr *attrValString var schemeClr *aSchemeClr - if color := stringPtr(formatSet.Series[i].Line.Color); *color != "" { + if color := stringPtr(opts.Series[i].Line.Color); *color != "" { *color = strings.TrimPrefix(*color, "#") srgbClr = &attrValString{Val: color} } else { - schemeClr = &aSchemeClr{Val: "accent" + strconv.Itoa((formatSet.order+i)%6+1)} + schemeClr = &aSchemeClr{Val: "accent" + strconv.Itoa((opts.order+i)%6+1)} } spPrScatter := &cSpPr{ @@ -786,7 +786,7 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { } spPrLine := &cSpPr{ Ln: &aLn{ - W: f.ptToEMUs(formatSet.Series[i].Line.Width), + W: f.ptToEMUs(opts.Series[i].Line.Width), Cap: "rnd", // rnd, sq, flat SolidFill: &aSolidFill{ SchemeClr: schemeClr, @@ -795,12 +795,12 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { }, } chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter} - return chartSeriesSpPr[formatSet.Type] + return chartSeriesSpPr[opts.Type] } // drawChartSeriesDPt provides a function to draw the c:dPt element by given // data index and format sets. -func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt { +func (f *File) drawChartSeriesDPt(i int, opts *chartOptions) []*cDPt { dpt := []*cDPt{{ IDx: &attrValInt{Val: intPtr(i)}, Bubble3D: &attrValBool{Val: boolPtr(false)}, @@ -824,19 +824,19 @@ func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt { }, }} chartSeriesDPt := map[string][]*cDPt{Pie: dpt, Pie3D: dpt} - return chartSeriesDPt[formatSet.Type] + return chartSeriesDPt[opts.Type] } // drawChartSeriesCat provides a function to draw the c:cat element by given // chart series and format sets. -func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) *cCat { +func (f *File) drawChartSeriesCat(v chartSeriesOptions, opts *chartOptions) *cCat { cat := &cCat{ StrRef: &cStrRef{ F: v.Categories, }, } chartSeriesCat := map[string]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil} - if _, ok := chartSeriesCat[formatSet.Type]; ok || v.Categories == "" { + if _, ok := chartSeriesCat[opts.Type]; ok || v.Categories == "" { return nil } return cat @@ -844,14 +844,14 @@ func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) * // drawChartSeriesVal provides a function to draw the c:val element by given // chart series and format sets. -func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) *cVal { +func (f *File) drawChartSeriesVal(v chartSeriesOptions, opts *chartOptions) *cVal { val := &cVal{ NumRef: &cNumRef{ F: v.Values, }, } chartSeriesVal := map[string]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil} - if _, ok := chartSeriesVal[formatSet.Type]; ok { + if _, ok := chartSeriesVal[opts.Type]; ok { return nil } return val @@ -859,16 +859,16 @@ func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) * // drawChartSeriesMarker provides a function to draw the c:marker element by // given data index and format sets. -func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { +func (f *File) drawChartSeriesMarker(i int, opts *chartOptions) *cMarker { defaultSymbol := map[string]*attrValString{Scatter: {Val: stringPtr("circle")}} marker := &cMarker{ - Symbol: defaultSymbol[formatSet.Type], + Symbol: defaultSymbol[opts.Type], Size: &attrValInt{Val: intPtr(5)}, } - if symbol := stringPtr(formatSet.Series[i].Marker.Symbol); *symbol != "" { + if symbol := stringPtr(opts.Series[i].Marker.Symbol); *symbol != "" { marker.Symbol = &attrValString{Val: symbol} } - if size := intPtr(formatSet.Series[i].Marker.Size); *size != 0 { + if size := intPtr(opts.Series[i].Marker.Size); *size != 0 { marker.Size = &attrValInt{Val: size} } if i < 6 { @@ -889,37 +889,37 @@ func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { } } chartSeriesMarker := map[string]*cMarker{Scatter: marker, Line: marker} - return chartSeriesMarker[formatSet.Type] + return chartSeriesMarker[opts.Type] } // drawChartSeriesXVal provides a function to draw the c:xVal element by given // chart series and format sets. -func (f *File) drawChartSeriesXVal(v formatChartSeries, formatSet *formatChart) *cCat { +func (f *File) drawChartSeriesXVal(v chartSeriesOptions, opts *chartOptions) *cCat { cat := &cCat{ StrRef: &cStrRef{ F: v.Categories, }, } chartSeriesXVal := map[string]*cCat{Scatter: cat} - return chartSeriesXVal[formatSet.Type] + return chartSeriesXVal[opts.Type] } // drawChartSeriesYVal provides a function to draw the c:yVal element by given // chart series and format sets. -func (f *File) drawChartSeriesYVal(v formatChartSeries, formatSet *formatChart) *cVal { +func (f *File) drawChartSeriesYVal(v chartSeriesOptions, opts *chartOptions) *cVal { val := &cVal{ NumRef: &cNumRef{ F: v.Values, }, } chartSeriesYVal := map[string]*cVal{Scatter: val, Bubble: val, Bubble3D: val} - return chartSeriesYVal[formatSet.Type] + return chartSeriesYVal[opts.Type] } // drawCharSeriesBubbleSize provides a function to draw the c:bubbleSize // element by given chart series and format sets. -func (f *File) drawCharSeriesBubbleSize(v formatChartSeries, formatSet *formatChart) *cVal { - if _, ok := map[string]bool{Bubble: true, Bubble3D: true}[formatSet.Type]; !ok { +func (f *File) drawCharSeriesBubbleSize(v chartSeriesOptions, opts *chartOptions) *cVal { + if _, ok := map[string]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok { return nil } return &cVal{ @@ -931,8 +931,8 @@ func (f *File) drawCharSeriesBubbleSize(v formatChartSeries, formatSet *formatCh // drawCharSeriesBubble3D provides a function to draw the c:bubble3D element // by given format sets. -func (f *File) drawCharSeriesBubble3D(formatSet *formatChart) *attrValBool { - if _, ok := map[string]bool{Bubble3D: true}[formatSet.Type]; !ok { +func (f *File) drawCharSeriesBubble3D(opts *chartOptions) *attrValBool { + if _, ok := map[string]bool{Bubble3D: true}[opts.Type]; !ok { return nil } return &attrValBool{Val: boolPtr(true)} @@ -940,51 +940,51 @@ func (f *File) drawCharSeriesBubble3D(formatSet *formatChart) *attrValBool { // drawChartDLbls provides a function to draw the c:dLbls element by given // format sets. -func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { +func (f *File) drawChartDLbls(opts *chartOptions) *cDLbls { return &cDLbls{ - ShowLegendKey: &attrValBool{Val: boolPtr(formatSet.Legend.ShowLegendKey)}, - ShowVal: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowVal)}, - ShowCatName: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowCatName)}, - ShowSerName: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowSerName)}, - ShowBubbleSize: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowBubbleSize)}, - ShowPercent: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowPercent)}, - ShowLeaderLines: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowLeaderLines)}, + ShowLegendKey: &attrValBool{Val: boolPtr(opts.Legend.ShowLegendKey)}, + ShowVal: &attrValBool{Val: boolPtr(opts.Plotarea.ShowVal)}, + ShowCatName: &attrValBool{Val: boolPtr(opts.Plotarea.ShowCatName)}, + ShowSerName: &attrValBool{Val: boolPtr(opts.Plotarea.ShowSerName)}, + ShowBubbleSize: &attrValBool{Val: boolPtr(opts.Plotarea.ShowBubbleSize)}, + ShowPercent: &attrValBool{Val: boolPtr(opts.Plotarea.ShowPercent)}, + ShowLeaderLines: &attrValBool{Val: boolPtr(opts.Plotarea.ShowLeaderLines)}, } } // drawChartSeriesDLbls provides a function to draw the c:dLbls element by // given format sets. -func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { - dLbls := f.drawChartDLbls(formatSet) +func (f *File) drawChartSeriesDLbls(opts *chartOptions) *cDLbls { + dLbls := f.drawChartDLbls(opts) chartSeriesDLbls := map[string]*cDLbls{ Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil, } - if _, ok := chartSeriesDLbls[formatSet.Type]; ok { + if _, ok := chartSeriesDLbls[opts.Type]; ok { return nil } return dLbls } // drawPlotAreaCatAx provides a function to draw the c:catAx element. -func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { - max := &attrValFloat{Val: formatSet.XAxis.Maximum} - min := &attrValFloat{Val: formatSet.XAxis.Minimum} - if formatSet.XAxis.Maximum == nil { +func (f *File) drawPlotAreaCatAx(opts *chartOptions) []*cAxs { + max := &attrValFloat{Val: opts.XAxis.Maximum} + min := &attrValFloat{Val: opts.XAxis.Minimum} + if opts.XAxis.Maximum == nil { max = nil } - if formatSet.XAxis.Minimum == nil { + if opts.XAxis.Minimum == nil { min = nil } axs := []*cAxs{ { AxID: &attrValInt{Val: intPtr(754001152)}, Scaling: &cScaling{ - Orientation: &attrValString{Val: stringPtr(orientation[formatSet.XAxis.ReverseOrder])}, + Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])}, Max: max, Min: min, }, - Delete: &attrValBool{Val: boolPtr(formatSet.XAxis.None)}, - AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, + Delete: &attrValBool{Val: boolPtr(opts.XAxis.None)}, + AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])}, NumFmt: &cNumFmt{ FormatCode: "General", SourceLinked: true, @@ -1002,45 +1002,45 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, }, } - if formatSet.XAxis.MajorGridlines { + if opts.XAxis.MajorGridlines { axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } - if formatSet.XAxis.MinorGridlines { + if opts.XAxis.MinorGridlines { axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } - if formatSet.XAxis.TickLabelSkip != 0 { - axs[0].TickLblSkip = &attrValInt{Val: intPtr(formatSet.XAxis.TickLabelSkip)} + if opts.XAxis.TickLabelSkip != 0 { + axs[0].TickLblSkip = &attrValInt{Val: intPtr(opts.XAxis.TickLabelSkip)} } return axs } // drawPlotAreaValAx provides a function to draw the c:valAx element. -func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { - max := &attrValFloat{Val: formatSet.YAxis.Maximum} - min := &attrValFloat{Val: formatSet.YAxis.Minimum} - if formatSet.YAxis.Maximum == nil { +func (f *File) drawPlotAreaValAx(opts *chartOptions) []*cAxs { + max := &attrValFloat{Val: opts.YAxis.Maximum} + min := &attrValFloat{Val: opts.YAxis.Minimum} + if opts.YAxis.Maximum == nil { max = nil } - if formatSet.YAxis.Minimum == nil { + if opts.YAxis.Minimum == nil { min = nil } var logBase *attrValFloat - if formatSet.YAxis.LogBase >= 2 && formatSet.YAxis.LogBase <= 1000 { - logBase = &attrValFloat{Val: float64Ptr(formatSet.YAxis.LogBase)} + if opts.YAxis.LogBase >= 2 && opts.YAxis.LogBase <= 1000 { + logBase = &attrValFloat{Val: float64Ptr(opts.YAxis.LogBase)} } axs := []*cAxs{ { AxID: &attrValInt{Val: intPtr(753999904)}, Scaling: &cScaling{ LogBase: logBase, - Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, + Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, Max: max, Min: min, }, - Delete: &attrValBool{Val: boolPtr(formatSet.YAxis.None)}, - AxPos: &attrValString{Val: stringPtr(valAxPos[formatSet.YAxis.ReverseOrder])}, + Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)}, + AxPos: &attrValString{Val: stringPtr(valAxPos[opts.YAxis.ReverseOrder])}, NumFmt: &cNumFmt{ - FormatCode: chartValAxNumFmtFormatCode[formatSet.Type], + FormatCode: chartValAxNumFmtFormatCode[opts.Type], SourceLinked: true, }, MajorTickMark: &attrValString{Val: stringPtr("none")}, @@ -1050,44 +1050,44 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { TxPr: f.drawPlotAreaTxPr(), CrossAx: &attrValInt{Val: intPtr(754001152)}, Crosses: &attrValString{Val: stringPtr("autoZero")}, - CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[formatSet.Type])}, + CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, }, } - if formatSet.YAxis.MajorGridlines { + if opts.YAxis.MajorGridlines { axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } - if formatSet.YAxis.MinorGridlines { + if opts.YAxis.MinorGridlines { axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } - if pos, ok := valTickLblPos[formatSet.Type]; ok { + if pos, ok := valTickLblPos[opts.Type]; ok { axs[0].TickLblPos.Val = stringPtr(pos) } - if formatSet.YAxis.MajorUnit != 0 { - axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.YAxis.MajorUnit)} + if opts.YAxis.MajorUnit != 0 { + axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(opts.YAxis.MajorUnit)} } return axs } // drawPlotAreaSerAx provides a function to draw the c:serAx element. -func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs { - max := &attrValFloat{Val: formatSet.YAxis.Maximum} - min := &attrValFloat{Val: formatSet.YAxis.Minimum} - if formatSet.YAxis.Maximum == nil { +func (f *File) drawPlotAreaSerAx(opts *chartOptions) []*cAxs { + max := &attrValFloat{Val: opts.YAxis.Maximum} + min := &attrValFloat{Val: opts.YAxis.Minimum} + if opts.YAxis.Maximum == nil { max = nil } - if formatSet.YAxis.Minimum == nil { + if opts.YAxis.Minimum == nil { min = nil } return []*cAxs{ { AxID: &attrValInt{Val: intPtr(832256642)}, Scaling: &cScaling{ - Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, + Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, Max: max, Min: min, }, - Delete: &attrValBool{Val: boolPtr(formatSet.YAxis.None)}, - AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, + Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)}, + AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])}, TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(), @@ -1207,7 +1207,7 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { // addDrawingChart provides a function to add chart graphic frame by given // sheet, drawingXML, cell, width, height, relationship index and format sets. -func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, formatSet *formatPicture) error { +func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, opts *pictureOptions) error { col, row, err := CellNameToCoordinates(cell) if err != nil { return err @@ -1215,17 +1215,17 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI colIdx := col - 1 rowIdx := row - 1 - width = int(float64(width) * formatSet.XScale) - height = int(float64(height) * formatSet.YScale) - colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.OffsetX, formatSet.OffsetY, width, height) + width = int(float64(width) * opts.XScale) + height = int(float64(height) * opts.YScale) + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.OffsetX, opts.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} - twoCellAnchor.EditAs = formatSet.Positioning + twoCellAnchor.EditAs = opts.Positioning from := xlsxFrom{} from.Col = colStart - from.ColOff = formatSet.OffsetX * EMU + from.ColOff = opts.OffsetX * EMU from.Row = rowStart - from.RowOff = formatSet.OffsetY * EMU + from.RowOff = opts.OffsetY * EMU to := xlsxTo{} to.Col = colEnd to.ColOff = x2 * EMU @@ -1255,8 +1255,8 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI graphic, _ := xml.Marshal(graphicFrame) twoCellAnchor.GraphicFrame = string(graphic) twoCellAnchor.ClientData = &xdrClientData{ - FLocksWithSheet: formatSet.FLocksWithSheet, - FPrintsWithSheet: formatSet.FPrintsWithSheet, + FLocksWithSheet: opts.FLocksWithSheet, + FPrintsWithSheet: opts.FPrintsWithSheet, } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) f.Drawings.Store(drawingXML, content) @@ -1266,10 +1266,10 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI // addSheetDrawingChart provides a function to add chart graphic frame for // chartsheet by given sheet, drawingXML, width, height, relationship index // and format sets. -func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *formatPicture) { +func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *pictureOptions) { content, cNvPrID := f.drawingParser(drawingXML) absoluteAnchor := xdrCellAnchor{ - EditAs: formatSet.Positioning, + EditAs: opts.Positioning, Pos: &xlsxPoint2D{}, Ext: &xlsxExt{}, } @@ -1295,8 +1295,8 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *forma graphic, _ := xml.Marshal(graphicFrame) absoluteAnchor.GraphicFrame = string(graphic) absoluteAnchor.ClientData = &xdrClientData{ - FLocksWithSheet: formatSet.FLocksWithSheet, - FPrintsWithSheet: formatSet.FPrintsWithSheet, + FLocksWithSheet: opts.FLocksWithSheet, + FPrintsWithSheet: opts.FPrintsWithSheet, } content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor) f.Drawings.Store(drawingXML, content) @@ -1304,8 +1304,9 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *forma // deleteDrawing provides a function to delete chart graphic frame by given by // given coordinates and graphic type. -func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (err error) { +func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) error { var ( + err error wsDr *xlsxWsDr deTwoCellAnchor *decodeTwoCellAnchor ) @@ -1331,7 +1332,7 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (err if err = f.xmlNewDecoder(strings.NewReader("" + wsDr.TwoCellAnchor[idx].GraphicFrame + "")). Decode(deTwoCellAnchor); err != nil && err != io.EOF { err = newDecodeXMLError(err) - return + return err } if err = nil; deTwoCellAnchor.From != nil && decodeTwoCellAnchorFuncs[drawingType](deTwoCellAnchor) { if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { diff --git a/excelize_test.go b/excelize_test.go index 9bb6fa83ae..e685b669ee 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -319,13 +319,13 @@ func TestNewFile(t *testing.T) { t.FailNow() } - // Test add picture to worksheet without formatset. + // Test add picture to worksheet without options. err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), "") if !assert.NoError(t, err) { t.FailNow() } - // Test add picture to worksheet with invalid formatset. + // Test add picture to worksheet with invalid options. err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), `{`) if !assert.Error(t, err) { t.FailNow() @@ -1021,12 +1021,6 @@ func TestRelsWriter(t *testing.T) { f.relsWriter() } -func TestGetSheetView(t *testing.T) { - f := NewFile() - _, err := f.getSheetView("SheetN", 0) - assert.EqualError(t, err, "sheet SheetN does not exist") -} - func TestConditionalFormat(t *testing.T) { f := NewFile() sheet1 := f.GetSheetName(0) @@ -1228,7 +1222,7 @@ func TestProtectSheet(t *testing.T) { sheetName := f.GetSheetName(0) assert.NoError(t, f.ProtectSheet(sheetName, nil)) // Test protect worksheet with XOR hash algorithm - assert.NoError(t, f.ProtectSheet(sheetName, &FormatSheetProtection{ + assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{ Password: "password", EditScenarios: false, })) @@ -1237,7 +1231,7 @@ func TestProtectSheet(t *testing.T) { assert.Equal(t, "83AF", ws.SheetProtection.Password) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestProtectSheet.xlsx"))) // Test protect worksheet with SHA-512 hash algorithm - assert.NoError(t, f.ProtectSheet(sheetName, &FormatSheetProtection{ + assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{ AlgorithmName: "SHA-512", Password: "password", })) @@ -1251,15 +1245,15 @@ func TestProtectSheet(t *testing.T) { // Test remove sheet protection with password verification assert.NoError(t, f.UnprotectSheet(sheetName, "password")) // Test protect worksheet with empty password - assert.NoError(t, f.ProtectSheet(sheetName, &FormatSheetProtection{})) + assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{})) assert.Equal(t, "", ws.SheetProtection.Password) // Test protect worksheet with password exceeds the limit length - assert.EqualError(t, f.ProtectSheet(sheetName, &FormatSheetProtection{ + assert.EqualError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{ AlgorithmName: "MD4", Password: strings.Repeat("s", MaxFieldLength+1), }), ErrPasswordLengthInvalid.Error()) // Test protect worksheet with unsupported hash algorithm - assert.EqualError(t, f.ProtectSheet(sheetName, &FormatSheetProtection{ + assert.EqualError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{ AlgorithmName: "RIPEMD-160", Password: "password", }), ErrUnsupportedHashAlgorithm.Error()) @@ -1282,13 +1276,13 @@ func TestUnprotectSheet(t *testing.T) { f = NewFile() sheetName := f.GetSheetName(0) - assert.NoError(t, f.ProtectSheet(sheetName, &FormatSheetProtection{Password: "password"})) + assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{Password: "password"})) // Test remove sheet protection with an incorrect password assert.EqualError(t, f.UnprotectSheet(sheetName, "wrongPassword"), ErrUnprotectSheetPassword.Error()) // Test remove sheet protection with password verification assert.NoError(t, f.UnprotectSheet(sheetName, "password")) // Test with invalid salt value - assert.NoError(t, f.ProtectSheet(sheetName, &FormatSheetProtection{ + assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{ AlgorithmName: "SHA-512", Password: "password", })) @@ -1309,7 +1303,7 @@ func TestSetDefaultTimeStyle(t *testing.T) { func TestAddVBAProject(t *testing.T) { f := NewFile() - assert.NoError(t, f.SetSheetPrOptions("Sheet1", CodeName("Sheet1"))) + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) assert.EqualError(t, f.AddVBAProject("macros.bin"), "stat macros.bin: no such file or directory") assert.EqualError(t, f.AddVBAProject(filepath.Join("test", "Book1.xlsx")), ErrAddVBAProject.Error()) assert.NoError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin"))) diff --git a/lib.go b/lib.go index 21ce7d2ca3..945c6f0f9c 100644 --- a/lib.go +++ b/lib.go @@ -422,20 +422,15 @@ func boolPtr(b bool) *bool { return &b } // intPtr returns a pointer to an int with the given value. func intPtr(i int) *int { return &i } +// uintPtr returns a pointer to an int with the given value. +func uintPtr(i uint) *uint { return &i } + // float64Ptr returns a pointer to a float64 with the given value. func float64Ptr(f float64) *float64 { return &f } // stringPtr returns a pointer to a string with the given value. func stringPtr(s string) *string { return &s } -// defaultTrue returns true if b is nil, or the pointed value. -func defaultTrue(b *bool) bool { - if b == nil { - return true - } - return *b -} - // MarshalXML convert the boolean data type to literal values 0 or 1 on // serialization. func (avb attrValBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error { @@ -499,11 +494,11 @@ func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err return nil } -// parseFormatSet provides a method to convert format string to []byte and +// fallbackOptions provides a method to convert format string to []byte and // handle empty string. -func parseFormatSet(formatSet string) []byte { - if formatSet != "" { - return []byte(formatSet) +func fallbackOptions(opts string) []byte { + if opts != "" { + return []byte(opts) } return []byte("{}") } diff --git a/picture.go b/picture.go index e1c62f2e52..aceb3f43bd 100644 --- a/picture.go +++ b/picture.go @@ -25,15 +25,15 @@ import ( "strings" ) -// parseFormatPictureSet provides a function to parse the format settings of +// parsePictureOptions provides a function to parse the format settings of // the picture with default value. -func parseFormatPictureSet(formatSet string) (*formatPicture, error) { - format := formatPicture{ +func parsePictureOptions(opts string) (*pictureOptions, error) { + format := pictureOptions{ FPrintsWithSheet: true, XScale: 1, YScale: 1, } - err := json.Unmarshal(parseFormatSet(formatSet), &format) + err := json.Unmarshal(fallbackOptions(opts), &format) return &format, err } @@ -148,14 +148,14 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // fmt.Println(err) // } // } -func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, file []byte) error { +func (f *File) AddPictureFromBytes(sheet, cell, opts, name, extension string, file []byte) error { var drawingHyperlinkRID int var hyperlinkType string ext, ok := supportedImageTypes[extension] if !ok { return ErrImgExt } - formatSet, err := parseFormatPictureSet(format) + options, err := parsePictureOptions(opts) if err != nil { return err } @@ -177,14 +177,14 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, mediaStr := ".." + strings.TrimPrefix(f.addMedia(file, ext), "xl") drawingRID := f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType) // Add picture with hyperlink. - if formatSet.Hyperlink != "" && formatSet.HyperlinkType != "" { - if formatSet.HyperlinkType == "External" { - hyperlinkType = formatSet.HyperlinkType + if options.Hyperlink != "" && options.HyperlinkType != "" { + if options.HyperlinkType == "External" { + hyperlinkType = options.HyperlinkType } - drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType) + drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, options.Hyperlink, hyperlinkType) } ws.Unlock() - err = f.addDrawingPicture(sheet, drawingXML, cell, name, img.Width, img.Height, drawingRID, drawingHyperlinkRID, formatSet) + err = f.addDrawingPicture(sheet, drawingXML, cell, name, img.Width, img.Height, drawingRID, drawingHyperlinkRID, options) if err != nil { return err } @@ -264,31 +264,31 @@ func (f *File) countDrawings() (count int) { // addDrawingPicture provides a function to add picture by given sheet, // drawingXML, cell, file name, width, height relationship index and format // sets. -func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, height, rID, hyperlinkRID int, formatSet *formatPicture) error { +func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, height, rID, hyperlinkRID int, opts *pictureOptions) error { col, row, err := CellNameToCoordinates(cell) if err != nil { return err } - if formatSet.Autofit { - width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), formatSet) + if opts.Autofit { + width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts) if err != nil { return err } } else { - width = int(float64(width) * formatSet.XScale) - height = int(float64(height) * formatSet.YScale) + width = int(float64(width) * opts.XScale) + height = int(float64(height) * opts.YScale) } col-- row-- - colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height) + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} - twoCellAnchor.EditAs = formatSet.Positioning + twoCellAnchor.EditAs = opts.Positioning from := xlsxFrom{} from.Col = colStart - from.ColOff = formatSet.OffsetX * EMU + from.ColOff = opts.OffsetX * EMU from.Row = rowStart - from.RowOff = formatSet.OffsetY * EMU + from.RowOff = opts.OffsetY * EMU to := xlsxTo{} to.Col = colEnd to.ColOff = x2 * EMU @@ -297,7 +297,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he twoCellAnchor.From = &from twoCellAnchor.To = &to pic := xlsxPic{} - pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = formatSet.NoChangeAspect + pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = opts.NoChangeAspect pic.NvPicPr.CNvPr.ID = cNvPrID pic.NvPicPr.CNvPr.Descr = file pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID) @@ -313,8 +313,8 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he twoCellAnchor.Pic = &pic twoCellAnchor.ClientData = &xdrClientData{ - FLocksWithSheet: formatSet.FLocksWithSheet, - FPrintsWithSheet: formatSet.FPrintsWithSheet, + FLocksWithSheet: opts.FLocksWithSheet, + FPrintsWithSheet: opts.FPrintsWithSheet, } content.Lock() defer content.Unlock() @@ -514,19 +514,19 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { // DeletePicture provides a function to delete charts in spreadsheet by given // worksheet name and cell reference. Note that the image file won't be deleted // from the document currently. -func (f *File) DeletePicture(sheet, cell string) (err error) { +func (f *File) DeletePicture(sheet, cell string) error { col, row, err := CellNameToCoordinates(cell) if err != nil { - return + return err } col-- row-- ws, err := f.workSheetReader(sheet) if err != nil { - return + return err } if ws.Drawing == nil { - return + return err } drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl") return f.deleteDrawing(col, row, drawingXML, "Pic") @@ -636,7 +636,7 @@ func (f *File) drawingsWriter() { } // drawingResize calculate the height and width after resizing. -func (f *File) drawingResize(sheet, cell string, width, height float64, formatSet *formatPicture) (w, h, c, r int, err error) { +func (f *File) drawingResize(sheet, cell string, width, height float64, opts *pictureOptions) (w, h, c, r int, err error) { var mergeCells []MergeCell mergeCells, err = f.GetMergeCells(sheet) if err != nil { @@ -678,7 +678,7 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, formatSe asp := float64(cellHeight) / height height, width = float64(cellHeight), width*asp } - width, height = width-float64(formatSet.OffsetX), height-float64(formatSet.OffsetY) - w, h = int(width*formatSet.XScale), int(height*formatSet.YScale) + width, height = width-float64(opts.OffsetX), height-float64(opts.OffsetY) + w, h = int(width*opts.XScale), int(height*opts.YScale) return } diff --git a/pivotTable.go b/pivotTable.go index 8e16e0689a..8266c8e67f 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -18,14 +18,14 @@ import ( "strings" ) -// PivotTableOption directly maps the format settings of the pivot table. +// PivotTableOptions directly maps the format settings of the pivot table. // // PivotTableStyleName: The built-in pivot table style names // // PivotStyleLight1 - PivotStyleLight28 // PivotStyleMedium1 - PivotStyleMedium28 // PivotStyleDark1 - PivotStyleDark28 -type PivotTableOption struct { +type PivotTableOptions struct { pivotTableSheetName string DataRange string `json:"data_range"` PivotTableRange string `json:"pivot_table_range"` @@ -81,9 +81,9 @@ type PivotTableField struct { // options. Note that the same fields can not in Columns, Rows and Filter // fields at the same time. // -// For example, create a pivot table on the Sheet1!$G$2:$M$34 range reference -// with the region Sheet1!$A$1:$E$31 as the data source, summarize by sum for -// sales: +// For example, create a pivot table on the range reference Sheet1!$G$2:$M$34 +// with the range reference Sheet1!$A$1:$E$31 as the data source, summarize by +// sum for sales: // // package main // @@ -129,7 +129,7 @@ type PivotTableField struct { // fmt.Println(err) // } // } -func (f *File) AddPivotTable(opts *PivotTableOption) error { +func (f *File) AddPivotTable(opts *PivotTableOptions) error { // parameter validation _, pivotTableSheetPath, err := f.parseFormatPivotTableSet(opts) if err != nil { @@ -168,7 +168,7 @@ func (f *File) AddPivotTable(opts *PivotTableOption) error { // parseFormatPivotTableSet provides a function to validate pivot table // properties. -func (f *File) parseFormatPivotTableSet(opts *PivotTableOption) (*xlsxWorksheet, string, error) { +func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet, string, error) { if opts == nil { return nil, "", ErrParameterRequired } @@ -228,7 +228,7 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { // getPivotFieldsOrder provides a function to get order list of pivot table // fields. -func (f *File) getPivotFieldsOrder(opts *PivotTableOption) ([]string, error) { +func (f *File) getPivotFieldsOrder(opts *PivotTableOptions) ([]string, error) { var order []string dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName) if dataRange == "" { @@ -250,7 +250,7 @@ func (f *File) getPivotFieldsOrder(opts *PivotTableOption) ([]string, error) { } // addPivotCache provides a function to create a pivot cache by given properties. -func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOption) error { +func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) error { // validate data range definedNameRef := true dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName) @@ -312,7 +312,7 @@ func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOption) error // addPivotTable provides a function to create a pivot table by given pivot // table ID and properties. -func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, opts *PivotTableOption) error { +func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, opts *PivotTableOptions) error { // validate pivot table range _, coordinates, err := f.adjustRange(opts.PivotTableRange) if err != nil { @@ -391,7 +391,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op // addPivotRowFields provides a method to add row fields for pivot table by // given pivot table options. -func (f *File) addPivotRowFields(pt *xlsxPivotTableDefinition, opts *PivotTableOption) error { +func (f *File) addPivotRowFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error { // row fields rowFieldsIndex, err := f.getPivotFieldsIndex(opts.Rows, opts) if err != nil { @@ -415,7 +415,7 @@ func (f *File) addPivotRowFields(pt *xlsxPivotTableDefinition, opts *PivotTableO // addPivotPageFields provides a method to add page fields for pivot table by // given pivot table options. -func (f *File) addPivotPageFields(pt *xlsxPivotTableDefinition, opts *PivotTableOption) error { +func (f *File) addPivotPageFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error { // page fields pageFieldsIndex, err := f.getPivotFieldsIndex(opts.Filter, opts) if err != nil { @@ -441,7 +441,7 @@ func (f *File) addPivotPageFields(pt *xlsxPivotTableDefinition, opts *PivotTable // addPivotDataFields provides a method to add data fields for pivot table by // given pivot table options. -func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTableOption) error { +func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error { // data fields dataFieldsIndex, err := f.getPivotFieldsIndex(opts.Data, opts) if err != nil { @@ -481,7 +481,7 @@ func inPivotTableField(a []PivotTableField, x string) int { // addPivotColFields create pivot column fields by given pivot table // definition and option. -func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opts *PivotTableOption) error { +func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error { if len(opts.Columns) == 0 { if len(opts.Data) <= 1 { return nil @@ -522,7 +522,7 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opts *PivotTableO // addPivotFields create pivot fields based on the column order of the first // row in the data region by given pivot table definition and option. -func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOption) error { +func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error { order, err := f.getPivotFieldsOrder(opts) if err != nil { return err @@ -627,7 +627,7 @@ func (f *File) countPivotCache() int { // getPivotFieldsIndex convert the column of the first row in the data region // to a sequential index by given fields and pivot option. -func (f *File) getPivotFieldsIndex(fields []PivotTableField, opts *PivotTableOption) ([]int, error) { +func (f *File) getPivotFieldsIndex(fields []PivotTableField, opts *PivotTableOptions) ([]int, error) { var pivotFieldsIndex []int orders, err := f.getPivotFieldsOrder(opts) if err != nil { diff --git a/pivotTable_test.go b/pivotTable_test.go index ed79298322..5d2e537853 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -25,7 +25,7 @@ func TestAddPivotTable(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000))) assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)])) } - assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$2:$M$34", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -41,7 +41,7 @@ func TestAddPivotTable(t *testing.T) { ShowError: true, })) // Use different order of coordinate tests - assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -55,7 +55,7 @@ func TestAddPivotTable(t *testing.T) { ShowLastColumn: true, })) - assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$W$2:$AC$34", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -68,7 +68,7 @@ func TestAddPivotTable(t *testing.T) { ShowColHeaders: true, ShowLastColumn: true, })) - assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$37:$W$50", Rows: []PivotTableField{{Data: "Month"}}, @@ -81,7 +81,7 @@ func TestAddPivotTable(t *testing.T) { ShowColHeaders: true, ShowLastColumn: true, })) - assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$AE$2:$AG$33", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -94,7 +94,7 @@ func TestAddPivotTable(t *testing.T) { ShowLastColumn: true, })) // Create pivot table with empty subtotal field name and specified style - assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$AJ$2:$AP1$35", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -110,7 +110,7 @@ func TestAddPivotTable(t *testing.T) { PivotTableStyleName: "PivotStyleLight19", })) f.NewSheet("Sheet2") - assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet2!$A$1:$AR$15", Rows: []PivotTableField{{Data: "Month"}}, @@ -123,7 +123,7 @@ func TestAddPivotTable(t *testing.T) { ShowColHeaders: true, ShowLastColumn: true, })) - assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet2!$A$18:$AR$54", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}}, @@ -143,7 +143,7 @@ func TestAddPivotTable(t *testing.T) { Comment: "Pivot Table Data Range", Scope: "Sheet2", })) - assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "dataRange", PivotTableRange: "Sheet2!$A$57:$AJ$91", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -160,7 +160,7 @@ func TestAddPivotTable(t *testing.T) { // Test empty pivot table options assert.EqualError(t, f.AddPivotTable(nil), ErrParameterRequired.Error()) // Test invalid data range - assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$A$1", PivotTableRange: "Sheet1!$U$34:$O$2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -168,7 +168,7 @@ func TestAddPivotTable(t *testing.T) { Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'DataRange' parsing error: parameter is invalid`) // Test the data range of the worksheet that is not declared - assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -176,7 +176,7 @@ func TestAddPivotTable(t *testing.T) { Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'DataRange' parsing error: parameter is invalid`) // Test the worksheet declared in the data range does not exist - assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "SheetN!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -184,7 +184,7 @@ func TestAddPivotTable(t *testing.T) { Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN does not exist") // Test the pivot table range of the worksheet that is not declared - assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "$U$34:$O$2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -192,7 +192,7 @@ func TestAddPivotTable(t *testing.T) { Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'PivotTableRange' parsing error: parameter is invalid`) // Test the worksheet declared in the pivot table range does not exist - assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "SheetN!$U$34:$O$2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -200,7 +200,7 @@ func TestAddPivotTable(t *testing.T) { Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN does not exist") // Test not exists worksheet in data range - assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "SheetN!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -208,7 +208,7 @@ func TestAddPivotTable(t *testing.T) { Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN does not exist") // Test invalid row number in data range - assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$0:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -217,7 +217,7 @@ func TestAddPivotTable(t *testing.T) { }), `parameter 'DataRange' parsing error: cannot convert cell "A0" to coordinates: invalid cell name "A0"`) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable1.xlsx"))) // Test with field names that exceed the length limit and invalid subtotal - assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$2:$M$34", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -232,12 +232,12 @@ func TestAddPivotTable(t *testing.T) { _, _, err = f.adjustRange("sheet1!") assert.EqualError(t, err, "parameter is invalid") // Test get pivot fields order with empty data range - _, err = f.getPivotFieldsOrder(&PivotTableOption{}) + _, err = f.getPivotFieldsOrder(&PivotTableOptions{}) assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) // Test add pivot cache with empty data range - assert.EqualError(t, f.addPivotCache("", &PivotTableOption{}), "parameter 'DataRange' parsing error: parameter is required") + assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required") // Test add pivot cache with invalid data range - assert.EqualError(t, f.addPivotCache("", &PivotTableOption{ + assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -245,11 +245,11 @@ func TestAddPivotTable(t *testing.T) { Data: []PivotTableField{{Data: "Sales"}}, }), "parameter 'DataRange' parsing error: parameter is invalid") // Test add pivot table with empty options - assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOption{}), "parameter 'PivotTableRange' parsing error: parameter is required") + assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required") // Test add pivot table with invalid data range - assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOption{}), "parameter 'PivotTableRange' parsing error: parameter is required") + assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required") // Test add pivot fields with empty data range - assert.EqualError(t, f.addPivotFields(nil, &PivotTableOption{ + assert.EqualError(t, f.addPivotFields(nil, &PivotTableOptions{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, @@ -257,14 +257,14 @@ func TestAddPivotTable(t *testing.T) { Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'DataRange' parsing error: parameter is invalid`) // Test get pivot fields index with empty data range - _, err = f.getPivotFieldsIndex([]PivotTableField{}, &PivotTableOption{}) + _, err = f.getPivotFieldsIndex([]PivotTableField{}, &PivotTableOptions{}) assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) } func TestAddPivotRowFields(t *testing.T) { f := NewFile() // Test invalid data range - assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ + assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ DataRange: "Sheet1!$A$1:$A$1", }), `parameter 'DataRange' parsing error: parameter is invalid`) } @@ -272,7 +272,7 @@ func TestAddPivotRowFields(t *testing.T) { func TestAddPivotPageFields(t *testing.T) { f := NewFile() // Test invalid data range - assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ + assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ DataRange: "Sheet1!$A$1:$A$1", }), `parameter 'DataRange' parsing error: parameter is invalid`) } @@ -280,7 +280,7 @@ func TestAddPivotPageFields(t *testing.T) { func TestAddPivotDataFields(t *testing.T) { f := NewFile() // Test invalid data range - assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ + assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ DataRange: "Sheet1!$A$1:$A$1", }), `parameter 'DataRange' parsing error: parameter is invalid`) } @@ -288,7 +288,7 @@ func TestAddPivotDataFields(t *testing.T) { func TestAddPivotColFields(t *testing.T) { f := NewFile() // Test invalid data range - assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ + assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ DataRange: "Sheet1!$A$1:$A$1", Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, }), `parameter 'DataRange' parsing error: parameter is invalid`) @@ -297,7 +297,7 @@ func TestAddPivotColFields(t *testing.T) { func TestGetPivotFieldsOrder(t *testing.T) { f := NewFile() // Test get pivot fields order with not exist worksheet - _, err := f.getPivotFieldsOrder(&PivotTableOption{DataRange: "SheetN!$A$1:$E$31"}) + _, err := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: "SheetN!$A$1:$E$31"}) assert.EqualError(t, err, "sheet SheetN does not exist") } diff --git a/rows_test.go b/rows_test.go index 8ce007ff85..74c4d25ffd 100644 --- a/rows_test.go +++ b/rows_test.go @@ -165,10 +165,10 @@ func TestRowHeight(t *testing.T) { assert.EqualError(t, err, "sheet SheetN does not exist") // Test get row height with custom default row height. - assert.NoError(t, f.SetSheetFormatPr(sheet1, - DefaultRowHeight(30.0), - CustomHeight(true), - )) + assert.NoError(t, f.SetSheetProps(sheet1, &SheetPropsOptions{ + DefaultRowHeight: float64Ptr(30.0), + CustomHeight: boolPtr(true), + })) height, err = f.GetRowHeight(sheet1, 100) assert.NoError(t, err) assert.Equal(t, 30.0, height) diff --git a/shape.go b/shape.go index 4fca348128..eca354f50d 100644 --- a/shape.go +++ b/shape.go @@ -17,21 +17,21 @@ import ( "strings" ) -// parseFormatShapeSet provides a function to parse the format settings of the +// parseShapeOptions provides a function to parse the format settings of the // shape with default value. -func parseFormatShapeSet(formatSet string) (*formatShape, error) { - format := formatShape{ +func parseShapeOptions(opts string) (*shapeOptions, error) { + options := shapeOptions{ Width: 160, Height: 160, - Format: formatPicture{ + Format: pictureOptions{ FPrintsWithSheet: true, XScale: 1, YScale: 1, }, - Line: formatLine{Width: 1}, + Line: lineOptions{Width: 1}, } - err := json.Unmarshal([]byte(formatSet), &format) - return &format, err + err := json.Unmarshal([]byte(opts), &options) + return &options, err } // AddShape provides the method to add shape in a sheet by given worksheet @@ -277,8 +277,8 @@ func parseFormatShapeSet(formatSet string) (*formatShape, error) { // wavy // wavyHeavy // wavyDbl -func (f *File) AddShape(sheet, cell, format string) error { - formatSet, err := parseFormatShapeSet(format) +func (f *File) AddShape(sheet, cell, opts string) error { + options, err := parseShapeOptions(opts) if err != nil { return err } @@ -305,7 +305,7 @@ func (f *File) AddShape(sheet, cell, format string) error { f.addSheetDrawing(sheet, rID) f.addSheetNameSpace(sheet, SourceRelationship) } - err = f.addDrawingShape(sheet, drawingXML, cell, formatSet) + err = f.addDrawingShape(sheet, drawingXML, cell, options) if err != nil { return err } @@ -315,7 +315,7 @@ func (f *File) AddShape(sheet, cell, format string) error { // addDrawingShape provides a function to add preset geometry by given sheet, // drawingXMLand format sets. -func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *formatShape) error { +func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOptions) error { fromCol, fromRow, err := CellNameToCoordinates(cell) if err != nil { return err @@ -344,19 +344,19 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format "wavyDbl": true, } - width := int(float64(formatSet.Width) * formatSet.Format.XScale) - height := int(float64(formatSet.Height) * formatSet.Format.YScale) + width := int(float64(opts.Width) * opts.Format.XScale) + height := int(float64(opts.Height) * opts.Format.YScale) - colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.Format.OffsetX, formatSet.Format.OffsetY, + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.Format.OffsetX, opts.Format.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} - twoCellAnchor.EditAs = formatSet.Format.Positioning + twoCellAnchor.EditAs = opts.Format.Positioning from := xlsxFrom{} from.Col = colStart - from.ColOff = formatSet.Format.OffsetX * EMU + from.ColOff = opts.Format.OffsetX * EMU from.Row = rowStart - from.RowOff = formatSet.Format.OffsetY * EMU + from.RowOff = opts.Format.OffsetY * EMU to := xlsxTo{} to.Col = colEnd to.ColOff = x2 * EMU @@ -365,7 +365,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format twoCellAnchor.From = &from twoCellAnchor.To = &to shape := xdrSp{ - Macro: formatSet.Macro, + Macro: opts.Macro, NvSpPr: &xdrNvSpPr{ CNvPr: &xlsxCNvPr{ ID: cNvPrID, @@ -377,13 +377,13 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format }, SpPr: &xlsxSpPr{ PrstGeom: xlsxPrstGeom{ - Prst: formatSet.Type, + Prst: opts.Type, }, }, Style: &xdrStyle{ - LnRef: setShapeRef(formatSet.Color.Line, 2), - FillRef: setShapeRef(formatSet.Color.Fill, 1), - EffectRef: setShapeRef(formatSet.Color.Effect, 0), + LnRef: setShapeRef(opts.Color.Line, 2), + FillRef: setShapeRef(opts.Color.Fill, 1), + EffectRef: setShapeRef(opts.Color.Effect, 0), FontRef: &aFontRef{ Idx: "minor", SchemeClr: &attrValString{ @@ -401,13 +401,13 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format }, }, } - if formatSet.Line.Width != 1 { + if opts.Line.Width != 1 { shape.SpPr.Ln = xlsxLineProperties{ - W: f.ptToEMUs(formatSet.Line.Width), + W: f.ptToEMUs(opts.Line.Width), } } - if len(formatSet.Paragraph) < 1 { - formatSet.Paragraph = []formatShapeParagraph{ + if len(opts.Paragraph) < 1 { + opts.Paragraph = []shapeParagraphOptions{ { Font: Font{ Bold: false, @@ -421,7 +421,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format }, } } - for _, p := range formatSet.Paragraph { + for _, p := range opts.Paragraph { u := p.Font.Underline _, ok := textUnderlineType[u] if !ok { @@ -460,8 +460,8 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format } twoCellAnchor.Sp = &shape twoCellAnchor.ClientData = &xdrClientData{ - FLocksWithSheet: formatSet.Format.FLocksWithSheet, - FPrintsWithSheet: formatSet.Format.FPrintsWithSheet, + FLocksWithSheet: opts.Format.FLocksWithSheet, + FPrintsWithSheet: opts.Format.FPrintsWithSheet, } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) f.Drawings.Store(drawingXML, content) diff --git a/sheet.go b/sheet.go index fe24b18c90..73e7501f27 100644 --- a/sheet.go +++ b/sheet.go @@ -38,10 +38,10 @@ import ( // Note that when creating a new workbook, the default worksheet named // `Sheet1` will be created. func (f *File) NewSheet(sheet string) int { - // Check if the worksheet already exists if trimSheetName(sheet) == "" { return -1 } + // Check if the worksheet already exists index := f.GetSheetIndex(sheet) if index != -1 { return index @@ -675,10 +675,10 @@ func (f *File) SetSheetVisible(sheet string, visible bool) error { return nil } -// parseFormatPanesSet provides a function to parse the panes settings. -func parseFormatPanesSet(formatSet string) (*formatPanes, error) { - format := formatPanes{} - err := json.Unmarshal([]byte(formatSet), &format) +// parsePanesOptions provides a function to parse the panes settings. +func parsePanesOptions(opts string) (*panesOptions, error) { + format := panesOptions{} + err := json.Unmarshal([]byte(opts), &format) return &format, err } @@ -767,7 +767,7 @@ func parseFormatPanesSet(formatSet string) (*formatPanes, error) { // // f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) func (f *File) SetPanes(sheet, panes string) error { - fs, _ := parseFormatPanesSet(panes) + fs, _ := parsePanesOptions(panes) ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -1021,7 +1021,7 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { // | // &R | Right section // | -// &S | Strikethrough text format +// &S | Strike through text format // | // &T | Current time // | @@ -1068,7 +1068,7 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { // that same page // // - No footer on the first page -func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error { +func (f *File) SetHeaderFooter(sheet string, settings *HeaderFooterOptions) error { ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -1113,13 +1113,13 @@ func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error // Password: "password", // EditScenarios: false, // }) -func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) error { +func (f *File) ProtectSheet(sheet string, settings *SheetProtectionOptions) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } if settings == nil { - settings = &FormatSheetProtection{ + settings = &SheetProtectionOptions{ EditObjects: true, EditScenarios: true, SelectLockedCells: true, @@ -1213,173 +1213,8 @@ func trimSheetName(name string) string { return name } -// PageLayoutOption is an option of a page layout of a worksheet. See -// SetPageLayout(). -type PageLayoutOption interface { - setPageLayout(layout *xlsxPageSetUp) -} - -// PageLayoutOptionPtr is a writable PageLayoutOption. See GetPageLayout(). -type PageLayoutOptionPtr interface { - PageLayoutOption - getPageLayout(layout *xlsxPageSetUp) -} - -type ( - // BlackAndWhite specified print black and white. - BlackAndWhite bool - // FirstPageNumber specified the first printed page number. If no value is - // specified, then 'automatic' is assumed. - FirstPageNumber uint - // PageLayoutOrientation defines the orientation of page layout for a - // worksheet. - PageLayoutOrientation string - // PageLayoutPaperSize defines the paper size of the worksheet. - PageLayoutPaperSize int - // FitToHeight specified the number of vertical pages to fit on. - FitToHeight int - // FitToWidth specified the number of horizontal pages to fit on. - FitToWidth int - // PageLayoutScale defines the print scaling. This attribute is restricted - // to value ranging from 10 (10%) to 400 (400%). This setting is - // overridden when fitToWidth and/or fitToHeight are in use. - PageLayoutScale uint -) - -const ( - // OrientationPortrait indicates page layout orientation id portrait. - OrientationPortrait = "portrait" - // OrientationLandscape indicates page layout orientation id landscape. - OrientationLandscape = "landscape" -) - -// setPageLayout provides a method to set the print black and white for the -// worksheet. -func (p BlackAndWhite) setPageLayout(ps *xlsxPageSetUp) { - ps.BlackAndWhite = bool(p) -} - -// getPageLayout provides a method to get the print black and white for the -// worksheet. -func (p *BlackAndWhite) getPageLayout(ps *xlsxPageSetUp) { - if ps == nil { - *p = false - return - } - *p = BlackAndWhite(ps.BlackAndWhite) -} - -// setPageLayout provides a method to set the first printed page number for -// the worksheet. -func (p FirstPageNumber) setPageLayout(ps *xlsxPageSetUp) { - if 0 < int(p) { - ps.FirstPageNumber = strconv.Itoa(int(p)) - ps.UseFirstPageNumber = true - } -} - -// getPageLayout provides a method to get the first printed page number for -// the worksheet. -func (p *FirstPageNumber) getPageLayout(ps *xlsxPageSetUp) { - if ps != nil && ps.UseFirstPageNumber { - if number, _ := strconv.Atoi(ps.FirstPageNumber); number != 0 { - *p = FirstPageNumber(number) - return - } - } - *p = 1 -} - -// setPageLayout provides a method to set the orientation for the worksheet. -func (o PageLayoutOrientation) setPageLayout(ps *xlsxPageSetUp) { - ps.Orientation = string(o) -} - -// getPageLayout provides a method to get the orientation for the worksheet. -func (o *PageLayoutOrientation) getPageLayout(ps *xlsxPageSetUp) { - // Excel default: portrait - if ps == nil || ps.Orientation == "" { - *o = OrientationPortrait - return - } - *o = PageLayoutOrientation(ps.Orientation) -} - -// setPageLayout provides a method to set the paper size for the worksheet. -func (p PageLayoutPaperSize) setPageLayout(ps *xlsxPageSetUp) { - ps.PaperSize = intPtr(int(p)) -} - -// getPageLayout provides a method to get the paper size for the worksheet. -func (p *PageLayoutPaperSize) getPageLayout(ps *xlsxPageSetUp) { - // Excel default: 1 - if ps == nil || ps.PaperSize == nil { - *p = 1 - return - } - *p = PageLayoutPaperSize(*ps.PaperSize) -} - -// setPageLayout provides a method to set the fit to height for the worksheet. -func (p FitToHeight) setPageLayout(ps *xlsxPageSetUp) { - if int(p) > 0 { - ps.FitToHeight = intPtr(int(p)) - } -} - -// getPageLayout provides a method to get the fit to height for the worksheet. -func (p *FitToHeight) getPageLayout(ps *xlsxPageSetUp) { - if ps == nil || ps.FitToHeight == nil { - *p = 1 - return - } - *p = FitToHeight(*ps.FitToHeight) -} - -// setPageLayout provides a method to set the fit to width for the worksheet. -func (p FitToWidth) setPageLayout(ps *xlsxPageSetUp) { - if int(p) > 0 { - ps.FitToWidth = intPtr(int(p)) - } -} - -// getPageLayout provides a method to get the fit to width for the worksheet. -func (p *FitToWidth) getPageLayout(ps *xlsxPageSetUp) { - if ps == nil || ps.FitToWidth == nil { - *p = 1 - return - } - *p = FitToWidth(*ps.FitToWidth) -} - -// setPageLayout provides a method to set the scale for the worksheet. -func (p PageLayoutScale) setPageLayout(ps *xlsxPageSetUp) { - if 10 <= int(p) && int(p) <= 400 { - ps.Scale = int(p) - } -} - -// getPageLayout provides a method to get the scale for the worksheet. -func (p *PageLayoutScale) getPageLayout(ps *xlsxPageSetUp) { - if ps == nil || ps.Scale < 10 || ps.Scale > 400 { - *p = 100 - return - } - *p = PageLayoutScale(ps.Scale) -} - // SetPageLayout provides a function to sets worksheet page layout. // -// Available options: -// -// BlackAndWhite(bool) -// FirstPageNumber(uint) -// PageLayoutOrientation(string) -// PageLayoutPaperSize(int) -// FitToHeight(int) -// FitToWidth(int) -// PageLayoutScale(uint) -// // The following shows the paper size sorted by excelize index number: // // Index | Paper Size @@ -1500,42 +1335,93 @@ func (p *PageLayoutScale) getPageLayout(ps *xlsxPageSetUp) { // 116 | PRC Envelope #8 Rotated (309 mm x 120 mm) // 117 | PRC Envelope #9 Rotated (324 mm x 229 mm) // 118 | PRC Envelope #10 Rotated (458 mm x 324 mm) -func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error { - s, err := f.workSheetReader(sheet) +func (f *File) SetPageLayout(sheet string, opts *PageLayoutOptions) error { + ws, err := f.workSheetReader(sheet) if err != nil { return err } - ps := s.PageSetUp - if ps == nil { - ps = new(xlsxPageSetUp) - s.PageSetUp = ps + if opts == nil { + return err + } + ws.setPageSetUp(opts) + return err +} + +// newPageSetUp initialize page setup settings for the worksheet if which not +// exist. +func (ws *xlsxWorksheet) newPageSetUp() { + if ws.PageSetUp == nil { + ws.PageSetUp = new(xlsxPageSetUp) } +} - for _, opt := range opts { - opt.setPageLayout(ps) +// setPageSetUp set page setup settings for the worksheet by given options. +func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) { + if opts.Size != nil { + ws.newPageSetUp() + ws.PageSetUp.PaperSize = opts.Size + } + if opts.Orientation != nil && (*opts.Orientation == "portrait" || *opts.Orientation == "landscape") { + ws.newPageSetUp() + ws.PageSetUp.Orientation = *opts.Orientation + } + if opts.FirstPageNumber != nil && *opts.FirstPageNumber > 0 { + ws.newPageSetUp() + ws.PageSetUp.FirstPageNumber = strconv.Itoa(int(*opts.FirstPageNumber)) + ws.PageSetUp.UseFirstPageNumber = true + } + if opts.AdjustTo != nil && 10 <= *opts.AdjustTo && *opts.AdjustTo <= 400 { + ws.newPageSetUp() + ws.PageSetUp.Scale = int(*opts.AdjustTo) + } + if opts.FitToHeight != nil { + ws.newPageSetUp() + ws.PageSetUp.FitToHeight = opts.FitToHeight + } + if opts.FitToWidth != nil { + ws.newPageSetUp() + ws.PageSetUp.FitToWidth = opts.FitToWidth + } + if opts.BlackAndWhite != nil { + ws.newPageSetUp() + ws.PageSetUp.BlackAndWhite = *opts.BlackAndWhite } - return err } // GetPageLayout provides a function to gets worksheet page layout. -// -// Available options: -// -// PageLayoutOrientation(string) -// PageLayoutPaperSize(int) -// FitToHeight(int) -// FitToWidth(int) -func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { - s, err := f.workSheetReader(sheet) +func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) { + opts := PageLayoutOptions{ + Size: intPtr(0), + Orientation: stringPtr("portrait"), + FirstPageNumber: uintPtr(1), + AdjustTo: uintPtr(100), + } + ws, err := f.workSheetReader(sheet) if err != nil { - return err + return opts, err } - ps := s.PageSetUp - - for _, opt := range opts { - opt.getPageLayout(ps) + if ws.PageSetUp != nil { + if ws.PageSetUp.PaperSize != nil { + opts.Size = ws.PageSetUp.PaperSize + } + if ws.PageSetUp.Orientation != "" { + opts.Orientation = stringPtr(ws.PageSetUp.Orientation) + } + if num, _ := strconv.Atoi(ws.PageSetUp.FirstPageNumber); num != 0 { + opts.FirstPageNumber = uintPtr(uint(num)) + } + if ws.PageSetUp.Scale >= 10 && ws.PageSetUp.Scale <= 400 { + opts.AdjustTo = uintPtr(uint(ws.PageSetUp.Scale)) + } + if ws.PageSetUp.FitToHeight != nil { + opts.FitToHeight = ws.PageSetUp.FitToHeight + } + if ws.PageSetUp.FitToWidth != nil { + opts.FitToWidth = ws.PageSetUp.FitToWidth + } + opts.BlackAndWhite = boolPtr(ws.PageSetUp.BlackAndWhite) } - return err + return opts, err } // SetDefinedName provides a function to set the defined names of the workbook @@ -1690,20 +1576,23 @@ func (f *File) UngroupSheets() error { // ends and where begins the next one by given worksheet name and cell reference, so the // content before the page break will be printed on one page and after the // page break on another. -func (f *File) InsertPageBreak(sheet, cell string) (err error) { - var ws *xlsxWorksheet - var row, col int +func (f *File) InsertPageBreak(sheet, cell string) error { + var ( + ws *xlsxWorksheet + row, col int + err error + ) rowBrk, colBrk := -1, -1 if ws, err = f.workSheetReader(sheet); err != nil { - return + return err } if col, row, err = CellNameToCoordinates(cell); err != nil { - return + return err } col-- row-- if col == row && col == 0 { - return + return err } if ws.RowBreaks == nil { ws.RowBreaks = &xlsxBreaks{} @@ -1741,24 +1630,27 @@ func (f *File) InsertPageBreak(sheet, cell string) (err error) { } ws.RowBreaks.Count = len(ws.RowBreaks.Brk) ws.ColBreaks.Count = len(ws.ColBreaks.Brk) - return + return err } // RemovePageBreak remove a page break by given worksheet name and cell // reference. -func (f *File) RemovePageBreak(sheet, cell string) (err error) { - var ws *xlsxWorksheet - var row, col int +func (f *File) RemovePageBreak(sheet, cell string) error { + var ( + ws *xlsxWorksheet + row, col int + err error + ) if ws, err = f.workSheetReader(sheet); err != nil { - return + return err } if col, row, err = CellNameToCoordinates(cell); err != nil { - return + return err } col-- row-- if col == row && col == 0 { - return + return err } removeBrk := func(ID int, brks []*xlsxBrk) []*xlsxBrk { for i, brk := range brks { @@ -1769,7 +1661,7 @@ func (f *File) RemovePageBreak(sheet, cell string) (err error) { return brks } if ws.RowBreaks == nil || ws.ColBreaks == nil { - return + return err } rowBrks := len(ws.RowBreaks.Brk) colBrks := len(ws.ColBreaks.Brk) @@ -1780,20 +1672,20 @@ func (f *File) RemovePageBreak(sheet, cell string) (err error) { ws.ColBreaks.Count = len(ws.ColBreaks.Brk) ws.RowBreaks.ManualBreakCount-- ws.ColBreaks.ManualBreakCount-- - return + return err } if rowBrks > 0 && rowBrks > colBrks { ws.RowBreaks.Brk = removeBrk(row, ws.RowBreaks.Brk) ws.RowBreaks.Count = len(ws.RowBreaks.Brk) ws.RowBreaks.ManualBreakCount-- - return + return err } if colBrks > 0 && colBrks > rowBrks { ws.ColBreaks.Brk = removeBrk(col, ws.ColBreaks.Brk) ws.ColBreaks.Count = len(ws.ColBreaks.Brk) ws.ColBreaks.ManualBreakCount-- } - return + return err } // relsReader provides a function to get the pointer to the structure diff --git a/sheet_test.go b/sheet_test.go index 87c36d469f..74ca02c1b3 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -8,78 +8,9 @@ import ( "strings" "testing" - "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" ) -func ExampleFile_SetPageLayout() { - f := NewFile() - if err := f.SetPageLayout( - "Sheet1", - BlackAndWhite(true), - FirstPageNumber(2), - PageLayoutOrientation(OrientationLandscape), - PageLayoutPaperSize(10), - FitToHeight(2), - FitToWidth(2), - PageLayoutScale(50), - ); err != nil { - fmt.Println(err) - } - // Output: -} - -func ExampleFile_GetPageLayout() { - f := NewFile() - var ( - blackAndWhite BlackAndWhite - firstPageNumber FirstPageNumber - orientation PageLayoutOrientation - paperSize PageLayoutPaperSize - fitToHeight FitToHeight - fitToWidth FitToWidth - scale PageLayoutScale - ) - if err := f.GetPageLayout("Sheet1", &blackAndWhite); err != nil { - fmt.Println(err) - } - if err := f.GetPageLayout("Sheet1", &firstPageNumber); err != nil { - fmt.Println(err) - } - if err := f.GetPageLayout("Sheet1", &orientation); err != nil { - fmt.Println(err) - } - if err := f.GetPageLayout("Sheet1", &paperSize); err != nil { - fmt.Println(err) - } - if err := f.GetPageLayout("Sheet1", &fitToHeight); err != nil { - fmt.Println(err) - } - if err := f.GetPageLayout("Sheet1", &fitToWidth); err != nil { - fmt.Println(err) - } - if err := f.GetPageLayout("Sheet1", &scale); err != nil { - fmt.Println(err) - } - fmt.Println("Defaults:") - fmt.Printf("- print black and white: %t\n", blackAndWhite) - fmt.Printf("- page number for first printed page: %d\n", firstPageNumber) - fmt.Printf("- orientation: %q\n", orientation) - fmt.Printf("- paper size: %d\n", paperSize) - fmt.Printf("- fit to height: %d\n", fitToHeight) - fmt.Printf("- fit to width: %d\n", fitToWidth) - fmt.Printf("- scale: %d\n", scale) - // Output: - // Defaults: - // - print black and white: false - // - page number for first printed page: 1 - // - orientation: "portrait" - // - paper size: 1 - // - fit to height: 1 - // - fit to width: 1 - // - scale: 100 -} - func TestNewSheet(t *testing.T) { f := NewFile() f.NewSheet("Sheet2") @@ -114,68 +45,6 @@ func TestSetPane(t *testing.T) { assert.NoError(t, f.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`)) } -func TestPageLayoutOption(t *testing.T) { - const sheet = "Sheet1" - - testData := []struct { - container PageLayoutOptionPtr - nonDefault PageLayoutOption - }{ - {new(BlackAndWhite), BlackAndWhite(true)}, - {new(FirstPageNumber), FirstPageNumber(2)}, - {new(PageLayoutOrientation), PageLayoutOrientation(OrientationLandscape)}, - {new(PageLayoutPaperSize), PageLayoutPaperSize(10)}, - {new(FitToHeight), FitToHeight(2)}, - {new(FitToWidth), FitToWidth(2)}, - {new(PageLayoutScale), PageLayoutScale(50)}, - } - - for i, test := range testData { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - opts := test.nonDefault - t.Logf("option %T", opts) - - def := deepcopy.Copy(test.container).(PageLayoutOptionPtr) - val1 := deepcopy.Copy(def).(PageLayoutOptionPtr) - val2 := deepcopy.Copy(def).(PageLayoutOptionPtr) - - f := NewFile() - // Get the default value - assert.NoError(t, f.GetPageLayout(sheet, def), opts) - // Get again and check - assert.NoError(t, f.GetPageLayout(sheet, val1), opts) - if !assert.Equal(t, val1, def, opts) { - t.FailNow() - } - // Set the same value - assert.NoError(t, f.SetPageLayout(sheet, val1), opts) - // Get again and check - assert.NoError(t, f.GetPageLayout(sheet, val1), opts) - if !assert.Equal(t, val1, def, "%T: value should not have changed", opts) { - t.FailNow() - } - // Set a different value - assert.NoError(t, f.SetPageLayout(sheet, test.nonDefault), opts) - assert.NoError(t, f.GetPageLayout(sheet, val1), opts) - // Get again and compare - assert.NoError(t, f.GetPageLayout(sheet, val2), opts) - if !assert.Equal(t, val1, val2, "%T: value should not have changed", opts) { - t.FailNow() - } - // Value should not be the same as the default - if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opts) { - t.FailNow() - } - // Restore the default value - assert.NoError(t, f.SetPageLayout(sheet, def), opts) - assert.NoError(t, f.GetPageLayout(sheet, val1), opts) - if !assert.Equal(t, def, val1) { - t.FailNow() - } - }) - } -} - func TestSearchSheet(t *testing.T) { f, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) if !assert.NoError(t, err) { @@ -226,14 +95,32 @@ func TestSearchSheet(t *testing.T) { func TestSetPageLayout(t *testing.T) { f := NewFile() + assert.NoError(t, f.SetPageLayout("Sheet1", nil)) + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).PageSetUp = nil + expected := PageLayoutOptions{ + Size: intPtr(1), + Orientation: stringPtr("landscape"), + FirstPageNumber: uintPtr(1), + AdjustTo: uintPtr(120), + FitToHeight: intPtr(2), + FitToWidth: intPtr(2), + BlackAndWhite: boolPtr(true), + } + assert.NoError(t, f.SetPageLayout("Sheet1", &expected)) + opts, err := f.GetPageLayout("Sheet1") + assert.NoError(t, err) + assert.Equal(t, expected, opts) // Test set page layout on not exists worksheet. - assert.EqualError(t, f.SetPageLayout("SheetN"), "sheet SheetN does not exist") + assert.EqualError(t, f.SetPageLayout("SheetN", nil), "sheet SheetN does not exist") } func TestGetPageLayout(t *testing.T) { f := NewFile() // Test get page layout on not exists worksheet. - assert.EqualError(t, f.GetPageLayout("SheetN"), "sheet SheetN does not exist") + _, err := f.GetPageLayout("SheetN") + assert.EqualError(t, err, "sheet SheetN does not exist") } func TestSetHeaderFooter(t *testing.T) { @@ -242,20 +129,20 @@ func TestSetHeaderFooter(t *testing.T) { // Test set header and footer on not exists worksheet. assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN does not exist") // Test set header and footer with illegal setting. - assert.EqualError(t, f.SetHeaderFooter("Sheet1", &FormatHeaderFooter{ + assert.EqualError(t, f.SetHeaderFooter("Sheet1", &HeaderFooterOptions{ OddHeader: strings.Repeat("c", MaxFieldLength+1), }), newFieldLengthError("OddHeader").Error()) assert.NoError(t, f.SetHeaderFooter("Sheet1", nil)) text := strings.Repeat("一", MaxFieldLength) - assert.NoError(t, f.SetHeaderFooter("Sheet1", &FormatHeaderFooter{ + assert.NoError(t, f.SetHeaderFooter("Sheet1", &HeaderFooterOptions{ OddHeader: text, OddFooter: text, EvenHeader: text, EvenFooter: text, FirstHeader: text, })) - assert.NoError(t, f.SetHeaderFooter("Sheet1", &FormatHeaderFooter{ + assert.NoError(t, f.SetHeaderFooter("Sheet1", &HeaderFooterOptions{ DifferentFirst: true, DifferentOddEven: true, OddHeader: "&R&P", diff --git a/sheetpr.go b/sheetpr.go index 8675c7548a..a246e9ef5a 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -11,647 +11,249 @@ package excelize -import "strings" - -// SheetPrOption is an option of a view of a worksheet. See SetSheetPrOptions(). -type SheetPrOption interface { - setSheetPrOption(view *xlsxSheetPr) -} - -// SheetPrOptionPtr is a writable SheetPrOption. See GetSheetPrOptions(). -type SheetPrOptionPtr interface { - SheetPrOption - getSheetPrOption(view *xlsxSheetPr) -} - -type ( - // CodeName is an option used for SheetPrOption and WorkbookPrOption - CodeName string - // EnableFormatConditionsCalculation is a SheetPrOption - EnableFormatConditionsCalculation bool - // Published is a SheetPrOption - Published bool - // FitToPage is a SheetPrOption - FitToPage bool - // TabColorIndexed is a TabColor option, within SheetPrOption - TabColorIndexed int - // TabColorRGB is a TabColor option, within SheetPrOption - TabColorRGB string - // TabColorTheme is a TabColor option, within SheetPrOption - TabColorTheme int - // TabColorTint is a TabColor option, within SheetPrOption - TabColorTint float64 - // AutoPageBreaks is a SheetPrOption - AutoPageBreaks bool - // OutlineSummaryBelow is an outlinePr, within SheetPr option - OutlineSummaryBelow bool -) - -// setSheetPrOption implements the SheetPrOption interface. -func (o OutlineSummaryBelow) setSheetPrOption(pr *xlsxSheetPr) { - if pr.OutlinePr == nil { - pr.OutlinePr = new(xlsxOutlinePr) +// SetPageMargins provides a function to set worksheet page margins. +func (f *File) SetPageMargins(sheet string, opts *PageLayoutMarginsOptions) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err } - pr.OutlinePr.SummaryBelow = bool(o) -} - -// getSheetPrOption implements the SheetPrOptionPtr interface. -func (o *OutlineSummaryBelow) getSheetPrOption(pr *xlsxSheetPr) { - // Excel default: true - if pr == nil || pr.OutlinePr == nil { - *o = true - return - } - *o = OutlineSummaryBelow(defaultTrue(&pr.OutlinePr.SummaryBelow)) -} - -// setSheetPrOption implements the SheetPrOption interface and specifies a -// stable name of the sheet. -func (o CodeName) setSheetPrOption(pr *xlsxSheetPr) { - pr.CodeName = string(o) -} - -// getSheetPrOption implements the SheetPrOptionPtr interface and get the -// stable name of the sheet. -func (o *CodeName) getSheetPrOption(pr *xlsxSheetPr) { - if pr == nil { - *o = "" - return - } - *o = CodeName(pr.CodeName) -} - -// setSheetPrOption implements the SheetPrOption interface and flag indicating -// whether the conditional formatting calculations shall be evaluated. -func (o EnableFormatConditionsCalculation) setSheetPrOption(pr *xlsxSheetPr) { - pr.EnableFormatConditionsCalculation = boolPtr(bool(o)) -} - -// getSheetPrOption implements the SheetPrOptionPtr interface and get the -// settings of whether the conditional formatting calculations shall be -// evaluated. -func (o *EnableFormatConditionsCalculation) getSheetPrOption(pr *xlsxSheetPr) { - if pr == nil { - *o = true - return - } - *o = EnableFormatConditionsCalculation(defaultTrue(pr.EnableFormatConditionsCalculation)) -} - -// setSheetPrOption implements the SheetPrOption interface and flag indicating -// whether the worksheet is published. -func (o Published) setSheetPrOption(pr *xlsxSheetPr) { - pr.Published = boolPtr(bool(o)) -} - -// getSheetPrOption implements the SheetPrOptionPtr interface and get the -// settings of whether the worksheet is published. -func (o *Published) getSheetPrOption(pr *xlsxSheetPr) { - if pr == nil { - *o = true - return - } - *o = Published(defaultTrue(pr.Published)) -} - -// setSheetPrOption implements the SheetPrOption interface. -func (o FitToPage) setSheetPrOption(pr *xlsxSheetPr) { - if pr.PageSetUpPr == nil { - if !o { - return - } - pr.PageSetUpPr = new(xlsxPageSetUpPr) + if opts == nil { + return err } - pr.PageSetUpPr.FitToPage = bool(o) -} - -// getSheetPrOption implements the SheetPrOptionPtr interface. -func (o *FitToPage) getSheetPrOption(pr *xlsxSheetPr) { - // Excel default: false - if pr == nil || pr.PageSetUpPr == nil { - *o = false - return - } - *o = FitToPage(pr.PageSetUpPr.FitToPage) -} - -// setSheetPrOption implements the SheetPrOption interface and sets the -// TabColor Indexed. -func (o TabColorIndexed) setSheetPrOption(pr *xlsxSheetPr) { - if pr.TabColor == nil { - pr.TabColor = new(xlsxTabColor) + preparePageMargins := func(ws *xlsxWorksheet) { + if ws.PageMargins == nil { + ws.PageMargins = new(xlsxPageMargins) + } } - pr.TabColor.Indexed = int(o) -} - -// getSheetPrOption implements the SheetPrOptionPtr interface and gets the -// TabColor Indexed. Defaults to -1 if no indexed has been set. -func (o *TabColorIndexed) getSheetPrOption(pr *xlsxSheetPr) { - if pr == nil || pr.TabColor == nil { - *o = TabColorIndexed(ColorMappingTypeUnset) - return - } - *o = TabColorIndexed(pr.TabColor.Indexed) -} - -// setSheetPrOption implements the SheetPrOption interface and specifies a -// stable name of the sheet. -func (o TabColorRGB) setSheetPrOption(pr *xlsxSheetPr) { - if pr.TabColor == nil { - if string(o) == "" { - return + preparePrintOptions := func(ws *xlsxWorksheet) { + if ws.PrintOptions == nil { + ws.PrintOptions = new(xlsxPrintOptions) } - pr.TabColor = new(xlsxTabColor) } - pr.TabColor.RGB = getPaletteColor(string(o)) -} - -// getSheetPrOption implements the SheetPrOptionPtr interface and get the -// stable name of the sheet. -func (o *TabColorRGB) getSheetPrOption(pr *xlsxSheetPr) { - if pr == nil || pr.TabColor == nil { - *o = "" - return - } - *o = TabColorRGB(strings.TrimPrefix(pr.TabColor.RGB, "FF")) -} - -// setSheetPrOption implements the SheetPrOption interface and sets the -// TabColor Theme. Warning: it does not create a clrScheme! -func (o TabColorTheme) setSheetPrOption(pr *xlsxSheetPr) { - if pr.TabColor == nil { - pr.TabColor = new(xlsxTabColor) + if opts.Bottom != nil { + preparePageMargins(ws) + ws.PageMargins.Bottom = *opts.Bottom } - pr.TabColor.Theme = int(o) -} - -// getSheetPrOption implements the SheetPrOptionPtr interface and gets the -// TabColor Theme. Defaults to -1 if no theme has been set. -func (o *TabColorTheme) getSheetPrOption(pr *xlsxSheetPr) { - if pr == nil || pr.TabColor == nil { - *o = TabColorTheme(ColorMappingTypeUnset) - return - } - *o = TabColorTheme(pr.TabColor.Theme) -} - -// setSheetPrOption implements the SheetPrOption interface and sets the -// TabColor Tint. -func (o TabColorTint) setSheetPrOption(pr *xlsxSheetPr) { - if pr.TabColor == nil { - pr.TabColor = new(xlsxTabColor) + if opts.Footer != nil { + preparePageMargins(ws) + ws.PageMargins.Footer = *opts.Footer } - pr.TabColor.Tint = float64(o) -} - -// getSheetPrOption implements the SheetPrOptionPtr interface and gets the -// TabColor Tint. Defaults to 0.0 if no tint has been set. -func (o *TabColorTint) getSheetPrOption(pr *xlsxSheetPr) { - if pr == nil || pr.TabColor == nil { - *o = 0.0 - return - } - *o = TabColorTint(pr.TabColor.Tint) -} - -// setSheetPrOption implements the SheetPrOption interface. -func (o AutoPageBreaks) setSheetPrOption(pr *xlsxSheetPr) { - if pr.PageSetUpPr == nil { - if !o { - return - } - pr.PageSetUpPr = new(xlsxPageSetUpPr) + if opts.Header != nil { + preparePageMargins(ws) + ws.PageMargins.Header = *opts.Header } - pr.PageSetUpPr.AutoPageBreaks = bool(o) -} - -// getSheetPrOption implements the SheetPrOptionPtr interface. -func (o *AutoPageBreaks) getSheetPrOption(pr *xlsxSheetPr) { - // Excel default: false - if pr == nil || pr.PageSetUpPr == nil { - *o = false - return - } - *o = AutoPageBreaks(pr.PageSetUpPr.AutoPageBreaks) -} - -// SetSheetPrOptions provides a function to sets worksheet properties. -// -// Available options: -// -// CodeName(string) -// EnableFormatConditionsCalculation(bool) -// Published(bool) -// FitToPage(bool) -// TabColorIndexed(int) -// TabColorRGB(string) -// TabColorTheme(int) -// TabColorTint(float64) -// AutoPageBreaks(bool) -// OutlineSummaryBelow(bool) -func (f *File) SetSheetPrOptions(sheet string, opts ...SheetPrOption) error { - ws, err := f.workSheetReader(sheet) - if err != nil { - return err + if opts.Left != nil { + preparePageMargins(ws) + ws.PageMargins.Left = *opts.Left } - pr := ws.SheetPr - if pr == nil { - pr = new(xlsxSheetPr) - ws.SheetPr = pr + if opts.Right != nil { + preparePageMargins(ws) + ws.PageMargins.Right = *opts.Right } - - for _, opt := range opts { - opt.setSheetPrOption(pr) + if opts.Top != nil { + preparePageMargins(ws) + ws.PageMargins.Top = *opts.Top + } + if opts.Horizontally != nil { + preparePrintOptions(ws) + ws.PrintOptions.HorizontalCentered = *opts.Horizontally + } + if opts.Vertically != nil { + preparePrintOptions(ws) + ws.PrintOptions.VerticalCentered = *opts.Vertically } return err } -// GetSheetPrOptions provides a function to gets worksheet properties. -// -// Available options: -// -// CodeName(string) -// EnableFormatConditionsCalculation(bool) -// Published(bool) -// FitToPage(bool) -// TabColorIndexed(int) -// TabColorRGB(string) -// TabColorTheme(int) -// TabColorTint(float64) -// AutoPageBreaks(bool) -// OutlineSummaryBelow(bool) -func (f *File) GetSheetPrOptions(sheet string, opts ...SheetPrOptionPtr) error { +// GetPageMargins provides a function to get worksheet page margins. +func (f *File) GetPageMargins(sheet string) (PageLayoutMarginsOptions, error) { + opts := PageLayoutMarginsOptions{ + Bottom: float64Ptr(0.75), + Footer: float64Ptr(0.3), + Header: float64Ptr(0.3), + Left: float64Ptr(0.7), + Right: float64Ptr(0.7), + Top: float64Ptr(0.75), + } ws, err := f.workSheetReader(sheet) if err != nil { - return err + return opts, err } - pr := ws.SheetPr - - for _, opt := range opts { - opt.getSheetPrOption(pr) + if ws.PageMargins != nil { + if ws.PageMargins.Bottom != 0 { + opts.Bottom = float64Ptr(ws.PageMargins.Bottom) + } + if ws.PageMargins.Footer != 0 { + opts.Footer = float64Ptr(ws.PageMargins.Footer) + } + if ws.PageMargins.Header != 0 { + opts.Header = float64Ptr(ws.PageMargins.Header) + } + if ws.PageMargins.Left != 0 { + opts.Left = float64Ptr(ws.PageMargins.Left) + } + if ws.PageMargins.Right != 0 { + opts.Right = float64Ptr(ws.PageMargins.Right) + } + if ws.PageMargins.Top != 0 { + opts.Top = float64Ptr(ws.PageMargins.Top) + } } - return err -} - -type ( - // PageMarginBottom specifies the bottom margin for the page. - PageMarginBottom float64 - // PageMarginFooter specifies the footer margin for the page. - PageMarginFooter float64 - // PageMarginHeader specifies the header margin for the page. - PageMarginHeader float64 - // PageMarginLeft specifies the left margin for the page. - PageMarginLeft float64 - // PageMarginRight specifies the right margin for the page. - PageMarginRight float64 - // PageMarginTop specifies the top margin for the page. - PageMarginTop float64 -) - -// setPageMargins provides a method to set the bottom margin for the worksheet. -func (p PageMarginBottom) setPageMargins(pm *xlsxPageMargins) { - pm.Bottom = float64(p) -} - -// setPageMargins provides a method to get the bottom margin for the worksheet. -func (p *PageMarginBottom) getPageMargins(pm *xlsxPageMargins) { - // Excel default: 0.75 - if pm == nil || pm.Bottom == 0 { - *p = 0.75 - return - } - *p = PageMarginBottom(pm.Bottom) -} - -// setPageMargins provides a method to set the footer margin for the worksheet. -func (p PageMarginFooter) setPageMargins(pm *xlsxPageMargins) { - pm.Footer = float64(p) -} - -// setPageMargins provides a method to get the footer margin for the worksheet. -func (p *PageMarginFooter) getPageMargins(pm *xlsxPageMargins) { - // Excel default: 0.3 - if pm == nil || pm.Footer == 0 { - *p = 0.3 - return - } - *p = PageMarginFooter(pm.Footer) -} - -// setPageMargins provides a method to set the header margin for the worksheet. -func (p PageMarginHeader) setPageMargins(pm *xlsxPageMargins) { - pm.Header = float64(p) -} - -// setPageMargins provides a method to get the header margin for the worksheet. -func (p *PageMarginHeader) getPageMargins(pm *xlsxPageMargins) { - // Excel default: 0.3 - if pm == nil || pm.Header == 0 { - *p = 0.3 - return - } - *p = PageMarginHeader(pm.Header) -} - -// setPageMargins provides a method to set the left margin for the worksheet. -func (p PageMarginLeft) setPageMargins(pm *xlsxPageMargins) { - pm.Left = float64(p) -} - -// setPageMargins provides a method to get the left margin for the worksheet. -func (p *PageMarginLeft) getPageMargins(pm *xlsxPageMargins) { - // Excel default: 0.7 - if pm == nil || pm.Left == 0 { - *p = 0.7 - return - } - *p = PageMarginLeft(pm.Left) -} - -// setPageMargins provides a method to set the right margin for the worksheet. -func (p PageMarginRight) setPageMargins(pm *xlsxPageMargins) { - pm.Right = float64(p) -} - -// setPageMargins provides a method to get the right margin for the worksheet. -func (p *PageMarginRight) getPageMargins(pm *xlsxPageMargins) { - // Excel default: 0.7 - if pm == nil || pm.Right == 0 { - *p = 0.7 - return - } - *p = PageMarginRight(pm.Right) -} - -// setPageMargins provides a method to set the top margin for the worksheet. -func (p PageMarginTop) setPageMargins(pm *xlsxPageMargins) { - pm.Top = float64(p) -} - -// setPageMargins provides a method to get the top margin for the worksheet. -func (p *PageMarginTop) getPageMargins(pm *xlsxPageMargins) { - // Excel default: 0.75 - if pm == nil || pm.Top == 0 { - *p = 0.75 - return - } - *p = PageMarginTop(pm.Top) -} - -// PageMarginsOptions is an option of a page margin of a worksheet. See -// SetPageMargins(). -type PageMarginsOptions interface { - setPageMargins(layout *xlsxPageMargins) -} - -// PageMarginsOptionsPtr is a writable PageMarginsOptions. See -// GetPageMargins(). -type PageMarginsOptionsPtr interface { - PageMarginsOptions - getPageMargins(layout *xlsxPageMargins) + if ws.PrintOptions != nil { + opts.Horizontally = boolPtr(ws.PrintOptions.HorizontalCentered) + opts.Vertically = boolPtr(ws.PrintOptions.VerticalCentered) + } + return opts, err } -// SetPageMargins provides a function to set worksheet page margins. -// -// Available options: -// -// PageMarginBottom(float64) -// PageMarginFooter(float64) -// PageMarginHeader(float64) -// PageMarginLeft(float64) -// PageMarginRight(float64) -// PageMarginTop(float64) -func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error { - s, err := f.workSheetReader(sheet) - if err != nil { - return err +// setSheetProps set worksheet format properties by given options. +func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) { + prepareSheetPr := func(ws *xlsxWorksheet) { + if ws.SheetPr == nil { + ws.SheetPr = new(xlsxSheetPr) + } } - pm := s.PageMargins - if pm == nil { - pm = new(xlsxPageMargins) - s.PageMargins = pm + preparePageSetUpPr := func(ws *xlsxWorksheet) { + prepareSheetPr(ws) + if ws.SheetPr.PageSetUpPr == nil { + ws.SheetPr.PageSetUpPr = new(xlsxPageSetUpPr) + } } - - for _, opt := range opts { - opt.setPageMargins(pm) + prepareOutlinePr := func(ws *xlsxWorksheet) { + prepareSheetPr(ws) + if ws.SheetPr.OutlinePr == nil { + ws.SheetPr.OutlinePr = new(xlsxOutlinePr) + } } - return err -} - -// GetPageMargins provides a function to get worksheet page margins. -// -// Available options: -// -// PageMarginBottom(float64) -// PageMarginFooter(float64) -// PageMarginHeader(float64) -// PageMarginLeft(float64) -// PageMarginRight(float64) -// PageMarginTop(float64) -func (f *File) GetPageMargins(sheet string, opts ...PageMarginsOptionsPtr) error { - s, err := f.workSheetReader(sheet) - if err != nil { - return err + prepareTabColor := func(ws *xlsxWorksheet) { + prepareSheetPr(ws) + if ws.SheetPr.TabColor == nil { + ws.SheetPr.TabColor = new(xlsxTabColor) + } } - pm := s.PageMargins - - for _, opt := range opts { - opt.getPageMargins(pm) + if opts.CodeName != nil { + prepareSheetPr(ws) + ws.SheetPr.CodeName = *opts.CodeName } - return err -} - -// SheetFormatPrOptions is an option of the formatting properties of a -// worksheet. See SetSheetFormatPr(). -type SheetFormatPrOptions interface { - setSheetFormatPr(formatPr *xlsxSheetFormatPr) -} - -// SheetFormatPrOptionsPtr is a writable SheetFormatPrOptions. See -// GetSheetFormatPr(). -type SheetFormatPrOptionsPtr interface { - SheetFormatPrOptions - getSheetFormatPr(formatPr *xlsxSheetFormatPr) -} - -type ( - // BaseColWidth specifies the number of characters of the maximum digit width - // of the normal style's font. This value does not include margin padding or - // extra padding for gridlines. It is only the number of characters. - BaseColWidth uint8 - // DefaultColWidth specifies the default column width measured as the number - // of characters of the maximum digit width of the normal style's font. - DefaultColWidth float64 - // DefaultRowHeight specifies the default row height measured in point size. - // Optimization so we don't have to write the height on all rows. This can be - // written out if most rows have custom height, to achieve the optimization. - DefaultRowHeight float64 - // CustomHeight specifies the custom height. - CustomHeight bool - // ZeroHeight specifies if rows are hidden. - ZeroHeight bool - // ThickTop specifies if rows have a thick top border by default. - ThickTop bool - // ThickBottom specifies if rows have a thick bottom border by default. - ThickBottom bool -) - -// setSheetFormatPr provides a method to set the number of characters of the -// maximum digit width of the normal style's font. -func (p BaseColWidth) setSheetFormatPr(fp *xlsxSheetFormatPr) { - fp.BaseColWidth = uint8(p) -} - -// setSheetFormatPr provides a method to set the number of characters of the -// maximum digit width of the normal style's font. -func (p *BaseColWidth) getSheetFormatPr(fp *xlsxSheetFormatPr) { - if fp == nil { - *p = 0 - return - } - *p = BaseColWidth(fp.BaseColWidth) -} - -// setSheetFormatPr provides a method to set the default column width measured -// as the number of characters of the maximum digit width of the normal -// style's font. -func (p DefaultColWidth) setSheetFormatPr(fp *xlsxSheetFormatPr) { - fp.DefaultColWidth = float64(p) -} - -// getSheetFormatPr provides a method to get the default column width measured -// as the number of characters of the maximum digit width of the normal -// style's font. -func (p *DefaultColWidth) getSheetFormatPr(fp *xlsxSheetFormatPr) { - if fp == nil { - *p = 0 - return - } - *p = DefaultColWidth(fp.DefaultColWidth) -} - -// setSheetFormatPr provides a method to set the default row height measured -// in point size. -func (p DefaultRowHeight) setSheetFormatPr(fp *xlsxSheetFormatPr) { - fp.DefaultRowHeight = float64(p) -} - -// getSheetFormatPr provides a method to get the default row height measured -// in point size. -func (p *DefaultRowHeight) getSheetFormatPr(fp *xlsxSheetFormatPr) { - if fp == nil { - *p = 15 - return - } - *p = DefaultRowHeight(fp.DefaultRowHeight) -} - -// setSheetFormatPr provides a method to set the custom height. -func (p CustomHeight) setSheetFormatPr(fp *xlsxSheetFormatPr) { - fp.CustomHeight = bool(p) -} - -// getSheetFormatPr provides a method to get the custom height. -func (p *CustomHeight) getSheetFormatPr(fp *xlsxSheetFormatPr) { - if fp == nil { - *p = false - return + if opts.EnableFormatConditionsCalculation != nil { + prepareSheetPr(ws) + ws.SheetPr.EnableFormatConditionsCalculation = opts.EnableFormatConditionsCalculation } - *p = CustomHeight(fp.CustomHeight) -} - -// setSheetFormatPr provides a method to set if rows are hidden. -func (p ZeroHeight) setSheetFormatPr(fp *xlsxSheetFormatPr) { - fp.ZeroHeight = bool(p) -} - -// getSheetFormatPr provides a method to get if rows are hidden. -func (p *ZeroHeight) getSheetFormatPr(fp *xlsxSheetFormatPr) { - if fp == nil { - *p = false - return + if opts.Published != nil { + prepareSheetPr(ws) + ws.SheetPr.Published = opts.Published + } + if opts.AutoPageBreaks != nil { + preparePageSetUpPr(ws) + ws.SheetPr.PageSetUpPr.AutoPageBreaks = *opts.AutoPageBreaks + } + if opts.FitToPage != nil { + preparePageSetUpPr(ws) + ws.SheetPr.PageSetUpPr.FitToPage = *opts.FitToPage + } + if opts.OutlineSummaryBelow != nil { + prepareOutlinePr(ws) + ws.SheetPr.OutlinePr.SummaryBelow = *opts.OutlineSummaryBelow + } + if opts.TabColorIndexed != nil { + prepareTabColor(ws) + ws.SheetPr.TabColor.Indexed = *opts.TabColorIndexed + } + if opts.TabColorRGB != nil { + prepareTabColor(ws) + ws.SheetPr.TabColor.RGB = *opts.TabColorRGB + } + if opts.TabColorTheme != nil { + prepareTabColor(ws) + ws.SheetPr.TabColor.Theme = *opts.TabColorTheme + } + if opts.TabColorTint != nil { + prepareTabColor(ws) + ws.SheetPr.TabColor.Tint = *opts.TabColorTint } - *p = ZeroHeight(fp.ZeroHeight) -} - -// setSheetFormatPr provides a method to set if rows have a thick top border -// by default. -func (p ThickTop) setSheetFormatPr(fp *xlsxSheetFormatPr) { - fp.ThickTop = bool(p) -} - -// getSheetFormatPr provides a method to get if rows have a thick top border -// by default. -func (p *ThickTop) getSheetFormatPr(fp *xlsxSheetFormatPr) { - if fp == nil { - *p = false - return - } - *p = ThickTop(fp.ThickTop) -} - -// setSheetFormatPr provides a method to set if rows have a thick bottom -// border by default. -func (p ThickBottom) setSheetFormatPr(fp *xlsxSheetFormatPr) { - fp.ThickBottom = bool(p) -} - -// setSheetFormatPr provides a method to set if rows have a thick bottom -// border by default. -func (p *ThickBottom) getSheetFormatPr(fp *xlsxSheetFormatPr) { - if fp == nil { - *p = false - return - } - *p = ThickBottom(fp.ThickBottom) } -// SetSheetFormatPr provides a function to set worksheet formatting properties. -// -// Available options: -// -// BaseColWidth(uint8) -// DefaultColWidth(float64) -// DefaultRowHeight(float64) -// CustomHeight(bool) -// ZeroHeight(bool) -// ThickTop(bool) -// ThickBottom(bool) -func (f *File) SetSheetFormatPr(sheet string, opts ...SheetFormatPrOptions) error { - s, err := f.workSheetReader(sheet) +// SetSheetProps provides a function to set worksheet properties. +func (f *File) SetSheetProps(sheet string, opts *SheetPropsOptions) error { + ws, err := f.workSheetReader(sheet) if err != nil { return err } - fp := s.SheetFormatPr - if fp == nil { - fp = new(xlsxSheetFormatPr) - s.SheetFormatPr = fp + if opts == nil { + return err + } + ws.setSheetProps(opts) + if ws.SheetFormatPr == nil { + ws.SheetFormatPr = &xlsxSheetFormatPr{DefaultRowHeight: defaultRowHeight} + } + if opts.BaseColWidth != nil { + ws.SheetFormatPr.BaseColWidth = *opts.BaseColWidth } - for _, opt := range opts { - opt.setSheetFormatPr(fp) + if opts.DefaultColWidth != nil { + ws.SheetFormatPr.DefaultColWidth = *opts.DefaultColWidth + } + if opts.DefaultRowHeight != nil { + ws.SheetFormatPr.DefaultRowHeight = *opts.DefaultRowHeight + } + if opts.CustomHeight != nil { + ws.SheetFormatPr.CustomHeight = *opts.CustomHeight + } + if opts.ZeroHeight != nil { + ws.SheetFormatPr.ZeroHeight = *opts.ZeroHeight + } + if opts.ThickTop != nil { + ws.SheetFormatPr.ThickTop = *opts.ThickTop + } + if opts.ThickBottom != nil { + ws.SheetFormatPr.ThickBottom = *opts.ThickBottom } return err } -// GetSheetFormatPr provides a function to get worksheet formatting properties. -// -// Available options: -// -// BaseColWidth(uint8) -// DefaultColWidth(float64) -// DefaultRowHeight(float64) -// CustomHeight(bool) -// ZeroHeight(bool) -// ThickTop(bool) -// ThickBottom(bool) -func (f *File) GetSheetFormatPr(sheet string, opts ...SheetFormatPrOptionsPtr) error { - s, err := f.workSheetReader(sheet) +// GetSheetProps provides a function to get worksheet properties. +func (f *File) GetSheetProps(sheet string) (SheetPropsOptions, error) { + baseColWidth := uint8(8) + opts := SheetPropsOptions{ + EnableFormatConditionsCalculation: boolPtr(true), + Published: boolPtr(true), + AutoPageBreaks: boolPtr(true), + OutlineSummaryBelow: boolPtr(true), + BaseColWidth: &baseColWidth, + } + ws, err := f.workSheetReader(sheet) if err != nil { - return err + return opts, err } - fp := s.SheetFormatPr - for _, opt := range opts { - opt.getSheetFormatPr(fp) + if ws.SheetPr != nil { + opts.CodeName = stringPtr(ws.SheetPr.CodeName) + if ws.SheetPr.EnableFormatConditionsCalculation != nil { + opts.EnableFormatConditionsCalculation = ws.SheetPr.EnableFormatConditionsCalculation + } + if ws.SheetPr.Published != nil { + opts.Published = ws.SheetPr.Published + } + if ws.SheetPr.PageSetUpPr != nil { + opts.AutoPageBreaks = boolPtr(ws.SheetPr.PageSetUpPr.AutoPageBreaks) + opts.FitToPage = boolPtr(ws.SheetPr.PageSetUpPr.FitToPage) + } + if ws.SheetPr.OutlinePr != nil { + opts.OutlineSummaryBelow = boolPtr(ws.SheetPr.OutlinePr.SummaryBelow) + } + if ws.SheetPr.TabColor != nil { + opts.TabColorIndexed = intPtr(ws.SheetPr.TabColor.Indexed) + opts.TabColorRGB = stringPtr(ws.SheetPr.TabColor.RGB) + opts.TabColorTheme = intPtr(ws.SheetPr.TabColor.Theme) + opts.TabColorTint = float64Ptr(ws.SheetPr.TabColor.Tint) + } } - return err + if ws.SheetFormatPr != nil { + opts.BaseColWidth = &ws.SheetFormatPr.BaseColWidth + opts.DefaultColWidth = float64Ptr(ws.SheetFormatPr.DefaultColWidth) + opts.DefaultRowHeight = float64Ptr(ws.SheetFormatPr.DefaultRowHeight) + opts.CustomHeight = boolPtr(ws.SheetFormatPr.CustomHeight) + opts.ZeroHeight = boolPtr(ws.SheetFormatPr.ZeroHeight) + opts.ThickTop = boolPtr(ws.SheetFormatPr.ThickTop) + opts.ThickBottom = boolPtr(ws.SheetFormatPr.ThickBottom) + } + return opts, err } diff --git a/sheetpr_test.go b/sheetpr_test.go index 291866806e..ccadbefcb2 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -1,501 +1,107 @@ package excelize import ( - "fmt" + "path/filepath" "testing" - "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" ) -var _ = []SheetPrOption{ - CodeName("hello"), - EnableFormatConditionsCalculation(false), - Published(false), - FitToPage(true), - TabColorIndexed(42), - TabColorRGB("#FFFF00"), - TabColorTheme(ColorMappingTypeLight2), - TabColorTint(0.5), - AutoPageBreaks(true), - OutlineSummaryBelow(true), -} - -var _ = []SheetPrOptionPtr{ - (*CodeName)(nil), - (*EnableFormatConditionsCalculation)(nil), - (*Published)(nil), - (*FitToPage)(nil), - (*TabColorIndexed)(nil), - (*TabColorRGB)(nil), - (*TabColorTheme)(nil), - (*TabColorTint)(nil), - (*AutoPageBreaks)(nil), - (*OutlineSummaryBelow)(nil), -} - -func ExampleFile_SetSheetPrOptions() { - f := NewFile() - const sheet = "Sheet1" - - if err := f.SetSheetPrOptions(sheet, - CodeName("code"), - EnableFormatConditionsCalculation(false), - Published(false), - FitToPage(true), - TabColorIndexed(42), - TabColorRGB("#FFFF00"), - TabColorTheme(ColorMappingTypeLight2), - TabColorTint(0.5), - AutoPageBreaks(true), - OutlineSummaryBelow(false), - ); err != nil { - fmt.Println(err) - } - // Output: -} - -func ExampleFile_GetSheetPrOptions() { - f := NewFile() - const sheet = "Sheet1" - - var ( - codeName CodeName - enableFormatConditionsCalculation EnableFormatConditionsCalculation - published Published - fitToPage FitToPage - tabColorIndexed TabColorIndexed - tabColorRGB TabColorRGB - tabColorTheme TabColorTheme - tabColorTint TabColorTint - autoPageBreaks AutoPageBreaks - outlineSummaryBelow OutlineSummaryBelow - ) - - if err := f.GetSheetPrOptions(sheet, - &codeName, - &enableFormatConditionsCalculation, - &published, - &fitToPage, - &tabColorIndexed, - &tabColorRGB, - &tabColorTheme, - &tabColorTint, - &autoPageBreaks, - &outlineSummaryBelow, - ); err != nil { - fmt.Println(err) - } - fmt.Println("Defaults:") - fmt.Printf("- codeName: %q\n", codeName) - fmt.Println("- enableFormatConditionsCalculation:", enableFormatConditionsCalculation) - fmt.Println("- published:", published) - fmt.Println("- fitToPage:", fitToPage) - fmt.Printf("- tabColorIndexed: %d\n", tabColorIndexed) - fmt.Printf("- tabColorRGB: %q\n", tabColorRGB) - fmt.Printf("- tabColorTheme: %d\n", tabColorTheme) - fmt.Printf("- tabColorTint: %f\n", tabColorTint) - fmt.Println("- autoPageBreaks:", autoPageBreaks) - fmt.Println("- outlineSummaryBelow:", outlineSummaryBelow) - // Output: - // Defaults: - // - codeName: "" - // - enableFormatConditionsCalculation: true - // - published: true - // - fitToPage: false - // - tabColorIndexed: -1 - // - tabColorRGB: "" - // - tabColorTheme: -1 - // - tabColorTint: 0.000000 - // - autoPageBreaks: false - // - outlineSummaryBelow: true -} - -func TestSheetPrOptions(t *testing.T) { - const sheet = "Sheet1" - - testData := []struct { - container SheetPrOptionPtr - nonDefault SheetPrOption - }{ - {new(CodeName), CodeName("xx")}, - {new(EnableFormatConditionsCalculation), EnableFormatConditionsCalculation(false)}, - {new(Published), Published(false)}, - {new(FitToPage), FitToPage(true)}, - {new(TabColorIndexed), TabColorIndexed(42)}, - {new(TabColorRGB), TabColorRGB("FFFF00")}, - {new(TabColorTheme), TabColorTheme(ColorMappingTypeLight2)}, - {new(TabColorTint), TabColorTint(0.5)}, - {new(AutoPageBreaks), AutoPageBreaks(true)}, - {new(OutlineSummaryBelow), OutlineSummaryBelow(false)}, - } - - for i, test := range testData { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - opts := test.nonDefault - t.Logf("option %T", opts) - - def := deepcopy.Copy(test.container).(SheetPrOptionPtr) - val1 := deepcopy.Copy(def).(SheetPrOptionPtr) - val2 := deepcopy.Copy(def).(SheetPrOptionPtr) - - f := NewFile() - // Get the default value - assert.NoError(t, f.GetSheetPrOptions(sheet, def), opts) - // Get again and check - assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opts) - if !assert.Equal(t, val1, def, opts) { - t.FailNow() - } - // Set the same value - assert.NoError(t, f.SetSheetPrOptions(sheet, val1), opts) - // Get again and check - assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opts) - if !assert.Equal(t, val1, def, "%T: value should not have changed", opts) { - t.FailNow() - } - // Set a different value - assert.NoError(t, f.SetSheetPrOptions(sheet, test.nonDefault), opts) - assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opts) - // Get again and compare - assert.NoError(t, f.GetSheetPrOptions(sheet, val2), opts) - if !assert.Equal(t, val1, val2, "%T: value should not have changed", opts) { - t.FailNow() - } - // Value should not be the same as the default - if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opts) { - t.FailNow() - } - // Restore the default value - assert.NoError(t, f.SetSheetPrOptions(sheet, def), opts) - assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opts) - if !assert.Equal(t, def, val1) { - t.FailNow() - } - }) - } -} - -func TestSetSheetPrOptions(t *testing.T) { - f := NewFile() - assert.NoError(t, f.SetSheetPrOptions("Sheet1", TabColorRGB(""))) - // Test SetSheetPrOptions on not exists worksheet. - assert.EqualError(t, f.SetSheetPrOptions("SheetN"), "sheet SheetN does not exist") -} - -func TestGetSheetPrOptions(t *testing.T) { - f := NewFile() - // Test GetSheetPrOptions on not exists worksheet. - assert.EqualError(t, f.GetSheetPrOptions("SheetN"), "sheet SheetN does not exist") -} - -var _ = []PageMarginsOptions{ - PageMarginBottom(1.0), - PageMarginFooter(1.0), - PageMarginHeader(1.0), - PageMarginLeft(1.0), - PageMarginRight(1.0), - PageMarginTop(1.0), -} - -var _ = []PageMarginsOptionsPtr{ - (*PageMarginBottom)(nil), - (*PageMarginFooter)(nil), - (*PageMarginHeader)(nil), - (*PageMarginLeft)(nil), - (*PageMarginRight)(nil), - (*PageMarginTop)(nil), -} - -func ExampleFile_SetPageMargins() { - f := NewFile() - const sheet = "Sheet1" - - if err := f.SetPageMargins(sheet, - PageMarginBottom(1.0), - PageMarginFooter(1.0), - PageMarginHeader(1.0), - PageMarginLeft(1.0), - PageMarginRight(1.0), - PageMarginTop(1.0), - ); err != nil { - fmt.Println(err) - } - // Output: -} - -func ExampleFile_GetPageMargins() { - f := NewFile() - const sheet = "Sheet1" - - var ( - marginBottom PageMarginBottom - marginFooter PageMarginFooter - marginHeader PageMarginHeader - marginLeft PageMarginLeft - marginRight PageMarginRight - marginTop PageMarginTop - ) - - if err := f.GetPageMargins(sheet, - &marginBottom, - &marginFooter, - &marginHeader, - &marginLeft, - &marginRight, - &marginTop, - ); err != nil { - fmt.Println(err) - } - fmt.Println("Defaults:") - fmt.Println("- marginBottom:", marginBottom) - fmt.Println("- marginFooter:", marginFooter) - fmt.Println("- marginHeader:", marginHeader) - fmt.Println("- marginLeft:", marginLeft) - fmt.Println("- marginRight:", marginRight) - fmt.Println("- marginTop:", marginTop) - // Output: - // Defaults: - // - marginBottom: 0.75 - // - marginFooter: 0.3 - // - marginHeader: 0.3 - // - marginLeft: 0.7 - // - marginRight: 0.7 - // - marginTop: 0.75 -} - -func TestPageMarginsOption(t *testing.T) { - const sheet = "Sheet1" - - testData := []struct { - container PageMarginsOptionsPtr - nonDefault PageMarginsOptions - }{ - {new(PageMarginTop), PageMarginTop(1.0)}, - {new(PageMarginBottom), PageMarginBottom(1.0)}, - {new(PageMarginLeft), PageMarginLeft(1.0)}, - {new(PageMarginRight), PageMarginRight(1.0)}, - {new(PageMarginHeader), PageMarginHeader(1.0)}, - {new(PageMarginFooter), PageMarginFooter(1.0)}, - } - - for i, test := range testData { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - opts := test.nonDefault - t.Logf("option %T", opts) - - def := deepcopy.Copy(test.container).(PageMarginsOptionsPtr) - val1 := deepcopy.Copy(def).(PageMarginsOptionsPtr) - val2 := deepcopy.Copy(def).(PageMarginsOptionsPtr) - - f := NewFile() - // Get the default value - assert.NoError(t, f.GetPageMargins(sheet, def), opts) - // Get again and check - assert.NoError(t, f.GetPageMargins(sheet, val1), opts) - if !assert.Equal(t, val1, def, opts) { - t.FailNow() - } - // Set the same value - assert.NoError(t, f.SetPageMargins(sheet, val1), opts) - // Get again and check - assert.NoError(t, f.GetPageMargins(sheet, val1), opts) - if !assert.Equal(t, val1, def, "%T: value should not have changed", opts) { - t.FailNow() - } - // Set a different value - assert.NoError(t, f.SetPageMargins(sheet, test.nonDefault), opts) - assert.NoError(t, f.GetPageMargins(sheet, val1), opts) - // Get again and compare - assert.NoError(t, f.GetPageMargins(sheet, val2), opts) - if !assert.Equal(t, val1, val2, "%T: value should not have changed", opts) { - t.FailNow() - } - // Value should not be the same as the default - if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opts) { - t.FailNow() - } - // Restore the default value - assert.NoError(t, f.SetPageMargins(sheet, def), opts) - assert.NoError(t, f.GetPageMargins(sheet, val1), opts) - if !assert.Equal(t, def, val1) { - t.FailNow() - } - }) - } -} - func TestSetPageMargins(t *testing.T) { f := NewFile() + assert.NoError(t, f.SetPageMargins("Sheet1", nil)) + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).PageMargins = nil + ws.(*xlsxWorksheet).PrintOptions = nil + expected := PageLayoutMarginsOptions{ + Bottom: float64Ptr(1.0), + Footer: float64Ptr(1.0), + Header: float64Ptr(1.0), + Left: float64Ptr(1.0), + Right: float64Ptr(1.0), + Top: float64Ptr(1.0), + Horizontally: boolPtr(true), + Vertically: boolPtr(true), + } + assert.NoError(t, f.SetPageMargins("Sheet1", &expected)) + opts, err := f.GetPageMargins("Sheet1") + assert.NoError(t, err) + assert.Equal(t, expected, opts) // Test set page margins on not exists worksheet. - assert.EqualError(t, f.SetPageMargins("SheetN"), "sheet SheetN does not exist") + assert.EqualError(t, f.SetPageMargins("SheetN", nil), "sheet SheetN does not exist") } func TestGetPageMargins(t *testing.T) { f := NewFile() // Test get page margins on not exists worksheet. - assert.EqualError(t, f.GetPageMargins("SheetN"), "sheet SheetN does not exist") -} - -func ExampleFile_SetSheetFormatPr() { - f := NewFile() - const sheet = "Sheet1" - - if err := f.SetSheetFormatPr(sheet, - BaseColWidth(1.0), - DefaultColWidth(1.0), - DefaultRowHeight(1.0), - CustomHeight(true), - ZeroHeight(true), - ThickTop(true), - ThickBottom(true), - ); err != nil { - fmt.Println(err) - } - // Output: + _, err := f.GetPageMargins("SheetN") + assert.EqualError(t, err, "sheet SheetN does not exist") } -func ExampleFile_GetSheetFormatPr() { +func TestDebug(t *testing.T) { f := NewFile() - const sheet = "Sheet1" - - var ( - baseColWidth BaseColWidth - defaultColWidth DefaultColWidth - defaultRowHeight DefaultRowHeight - customHeight CustomHeight - zeroHeight ZeroHeight - thickTop ThickTop - thickBottom ThickBottom - ) - - if err := f.GetSheetFormatPr(sheet, - &baseColWidth, - &defaultColWidth, - &defaultRowHeight, - &customHeight, - &zeroHeight, - &thickTop, - &thickBottom, - ); err != nil { - fmt.Println(err) - } - fmt.Println("Defaults:") - fmt.Println("- baseColWidth:", baseColWidth) - fmt.Println("- defaultColWidth:", defaultColWidth) - fmt.Println("- defaultRowHeight:", defaultRowHeight) - fmt.Println("- customHeight:", customHeight) - fmt.Println("- zeroHeight:", zeroHeight) - fmt.Println("- thickTop:", thickTop) - fmt.Println("- thickBottom:", thickBottom) - // Output: - // Defaults: - // - baseColWidth: 0 - // - defaultColWidth: 0 - // - defaultRowHeight: 15 - // - customHeight: false - // - zeroHeight: false - // - thickTop: false - // - thickBottom: false -} - -func TestSheetFormatPrOptions(t *testing.T) { - const sheet = "Sheet1" - - testData := []struct { - container SheetFormatPrOptionsPtr - nonDefault SheetFormatPrOptions - }{ - {new(BaseColWidth), BaseColWidth(1.0)}, - {new(DefaultColWidth), DefaultColWidth(1.0)}, - {new(DefaultRowHeight), DefaultRowHeight(1.0)}, - {new(CustomHeight), CustomHeight(true)}, - {new(ZeroHeight), ZeroHeight(true)}, - {new(ThickTop), ThickTop(true)}, - {new(ThickBottom), ThickBottom(true)}, - } - - for i, test := range testData { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - opts := test.nonDefault - t.Logf("option %T", opts) - - def := deepcopy.Copy(test.container).(SheetFormatPrOptionsPtr) - val1 := deepcopy.Copy(def).(SheetFormatPrOptionsPtr) - val2 := deepcopy.Copy(def).(SheetFormatPrOptionsPtr) - - f := NewFile() - // Get the default value - assert.NoError(t, f.GetSheetFormatPr(sheet, def), opts) - // Get again and check - assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opts) - if !assert.Equal(t, val1, def, opts) { - t.FailNow() - } - // Set the same value - assert.NoError(t, f.SetSheetFormatPr(sheet, val1), opts) - // Get again and check - assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opts) - if !assert.Equal(t, val1, def, "%T: value should not have changed", opts) { - t.FailNow() - } - // Set a different value - assert.NoError(t, f.SetSheetFormatPr(sheet, test.nonDefault), opts) - assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opts) - // Get again and compare - assert.NoError(t, f.GetSheetFormatPr(sheet, val2), opts) - if !assert.Equal(t, val1, val2, "%T: value should not have changed", opts) { - t.FailNow() - } - // Value should not be the same as the default - if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opts) { - t.FailNow() - } - // Restore the default value - assert.NoError(t, f.SetSheetFormatPr(sheet, def), opts) - assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opts) - if !assert.Equal(t, def, val1) { - t.FailNow() - } - }) - } -} - -func TestSetSheetFormatPr(t *testing.T) { - f := NewFile() - assert.NoError(t, f.GetSheetFormatPr("Sheet1")) + assert.NoError(t, f.SetSheetProps("Sheet1", nil)) ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) + ws.(*xlsxWorksheet).PageMargins = nil + ws.(*xlsxWorksheet).PrintOptions = nil + ws.(*xlsxWorksheet).SheetPr = nil ws.(*xlsxWorksheet).SheetFormatPr = nil - assert.NoError(t, f.SetSheetFormatPr("Sheet1", BaseColWidth(1.0))) - // Test set formatting properties on not exists worksheet. - assert.EqualError(t, f.SetSheetFormatPr("SheetN"), "sheet SheetN does not exist") + // w := uint8(10) + // f.SetSheetProps("Sheet1", &SheetPropsOptions{BaseColWidth: &w}) + f.SetPageMargins("Sheet1", &PageLayoutMarginsOptions{Horizontally: boolPtr(true)}) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDebug.xlsx"))) } -func TestGetSheetFormatPr(t *testing.T) { +func TestSetSheetProps(t *testing.T) { f := NewFile() - assert.NoError(t, f.GetSheetFormatPr("Sheet1")) + assert.NoError(t, f.SetSheetProps("Sheet1", nil)) ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) + ws.(*xlsxWorksheet).SheetPr = nil ws.(*xlsxWorksheet).SheetFormatPr = nil - var ( - baseColWidth BaseColWidth - defaultColWidth DefaultColWidth - defaultRowHeight DefaultRowHeight - customHeight CustomHeight - zeroHeight ZeroHeight - thickTop ThickTop - thickBottom ThickBottom - ) - assert.NoError(t, f.GetSheetFormatPr("Sheet1", - &baseColWidth, - &defaultColWidth, - &defaultRowHeight, - &customHeight, - &zeroHeight, - &thickTop, - &thickBottom, - )) - // Test get formatting properties on not exists worksheet. - assert.EqualError(t, f.GetSheetFormatPr("SheetN"), "sheet SheetN does not exist") + baseColWidth := uint8(8) + expected := SheetPropsOptions{ + CodeName: stringPtr("code"), + EnableFormatConditionsCalculation: boolPtr(true), + Published: boolPtr(true), + AutoPageBreaks: boolPtr(true), + FitToPage: boolPtr(true), + TabColorIndexed: intPtr(1), + TabColorRGB: stringPtr("#FFFF00"), + TabColorTheme: intPtr(1), + TabColorTint: float64Ptr(1), + OutlineSummaryBelow: boolPtr(true), + BaseColWidth: &baseColWidth, + DefaultColWidth: float64Ptr(10), + DefaultRowHeight: float64Ptr(10), + CustomHeight: boolPtr(true), + ZeroHeight: boolPtr(true), + ThickTop: boolPtr(true), + ThickBottom: boolPtr(true), + } + assert.NoError(t, f.SetSheetProps("Sheet1", &expected)) + opts, err := f.GetSheetProps("Sheet1") + assert.NoError(t, err) + assert.Equal(t, expected, opts) + + ws.(*xlsxWorksheet).SheetPr = nil + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{FitToPage: boolPtr(true)})) + ws.(*xlsxWorksheet).SheetPr = nil + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{TabColorRGB: stringPtr("#FFFF00")})) + ws.(*xlsxWorksheet).SheetPr = nil + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{TabColorTheme: intPtr(1)})) + ws.(*xlsxWorksheet).SheetPr = nil + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{TabColorTint: float64Ptr(1)})) + + // Test SetSheetProps on not exists worksheet. + assert.EqualError(t, f.SetSheetProps("SheetN", nil), "sheet SheetN does not exist") +} + +func TestGetSheetProps(t *testing.T) { + f := NewFile() + // Test GetSheetProps on not exists worksheet. + _, err := f.GetSheetProps("SheetN") + assert.EqualError(t, err, "sheet SheetN does not exist") } diff --git a/sheetview.go b/sheetview.go index 373658844c..a47d5100e5 100644 --- a/sheetview.go +++ b/sheetview.go @@ -13,150 +13,6 @@ package excelize import "fmt" -// SheetViewOption is an option of a view of a worksheet. See -// SetSheetViewOptions(). -type SheetViewOption interface { - setSheetViewOption(view *xlsxSheetView) -} - -// SheetViewOptionPtr is a writable SheetViewOption. See -// GetSheetViewOptions(). -type SheetViewOptionPtr interface { - SheetViewOption - getSheetViewOption(view *xlsxSheetView) -} - -type ( - // DefaultGridColor is a SheetViewOption. It specifies a flag indicating - // that the consuming application should use the default grid lines color - // (system dependent). Overrides any color specified in colorId. - DefaultGridColor bool - // ShowFormulas is a SheetViewOption. It specifies a flag indicating - // whether this sheet should display formulas. - ShowFormulas bool - // ShowGridLines is a SheetViewOption. It specifies a flag indicating - // whether this sheet should display gridlines. - ShowGridLines bool - // ShowRowColHeaders is a SheetViewOption. It specifies a flag indicating - // whether the sheet should display row and column headings. - ShowRowColHeaders bool - // ShowZeros is a SheetViewOption. It specifies a flag indicating whether - // to "show a zero in cells that have zero value". When using a formula to - // reference another cell which is empty, the referenced value becomes 0 - // when the flag is true. (Default setting is true.) - ShowZeros bool - // RightToLeft is a SheetViewOption. It specifies a flag indicating whether - // the sheet is in 'right to left' display mode. When in this mode, Column - // A is on the far right, Column B ;is one column left of Column A, and so - // on. Also, information in cells is displayed in the Right to Left format. - RightToLeft bool - // ShowRuler is a SheetViewOption. It specifies a flag indicating this - // sheet should display ruler. - ShowRuler bool - // View is a SheetViewOption. It specifies a flag indicating how sheet is - // displayed, by default it uses empty string available options: normal, - // pageLayout, pageBreakPreview - View string - // TopLeftCell is a SheetViewOption. It specifies a location of the top - // left visible cell Location of the top left visible cell in the bottom - // right pane (when in Left-to-Right mode). - TopLeftCell string - // ZoomScale is a SheetViewOption. It specifies a window zoom magnification - // for current view representing percent values. This attribute is - // restricted to values ranging from 10 to 400. Horizontal & Vertical - // scale together. - ZoomScale float64 -) - -// Defaults for each option are described in XML schema for CT_SheetView - -func (o DefaultGridColor) setSheetViewOption(view *xlsxSheetView) { - view.DefaultGridColor = boolPtr(bool(o)) -} - -func (o *DefaultGridColor) getSheetViewOption(view *xlsxSheetView) { - *o = DefaultGridColor(defaultTrue(view.DefaultGridColor)) // Excel default: true -} - -func (o ShowFormulas) setSheetViewOption(view *xlsxSheetView) { - view.ShowFormulas = bool(o) // Excel default: false -} - -func (o *ShowFormulas) getSheetViewOption(view *xlsxSheetView) { - *o = ShowFormulas(view.ShowFormulas) // Excel default: false -} - -func (o ShowGridLines) setSheetViewOption(view *xlsxSheetView) { - view.ShowGridLines = boolPtr(bool(o)) -} - -func (o *ShowGridLines) getSheetViewOption(view *xlsxSheetView) { - *o = ShowGridLines(defaultTrue(view.ShowGridLines)) // Excel default: true -} - -func (o ShowRowColHeaders) setSheetViewOption(view *xlsxSheetView) { - view.ShowRowColHeaders = boolPtr(bool(o)) -} - -func (o *ShowRowColHeaders) getSheetViewOption(view *xlsxSheetView) { - *o = ShowRowColHeaders(defaultTrue(view.ShowRowColHeaders)) // Excel default: true -} - -func (o ShowZeros) setSheetViewOption(view *xlsxSheetView) { - view.ShowZeros = boolPtr(bool(o)) -} - -func (o *ShowZeros) getSheetViewOption(view *xlsxSheetView) { - *o = ShowZeros(defaultTrue(view.ShowZeros)) // Excel default: true -} - -func (o RightToLeft) setSheetViewOption(view *xlsxSheetView) { - view.RightToLeft = bool(o) // Excel default: false -} - -func (o *RightToLeft) getSheetViewOption(view *xlsxSheetView) { - *o = RightToLeft(view.RightToLeft) -} - -func (o ShowRuler) setSheetViewOption(view *xlsxSheetView) { - view.ShowRuler = boolPtr(bool(o)) -} - -func (o *ShowRuler) getSheetViewOption(view *xlsxSheetView) { - *o = ShowRuler(defaultTrue(view.ShowRuler)) // Excel default: true -} - -func (o View) setSheetViewOption(view *xlsxSheetView) { - view.View = string(o) -} - -func (o *View) getSheetViewOption(view *xlsxSheetView) { - if view.View != "" { - *o = View(view.View) - return - } - *o = "normal" -} - -func (o TopLeftCell) setSheetViewOption(view *xlsxSheetView) { - view.TopLeftCell = string(o) -} - -func (o *TopLeftCell) getSheetViewOption(view *xlsxSheetView) { - *o = TopLeftCell(view.TopLeftCell) -} - -func (o ZoomScale) setSheetViewOption(view *xlsxSheetView) { - // This attribute is restricted to values ranging from 10 to 400. - if float64(o) >= 10 && float64(o) <= 400 { - view.ZoomScale = float64(o) - } -} - -func (o *ZoomScale) getSheetViewOption(view *xlsxSheetView) { - *o = ZoomScale(view.ZoomScale) -} - // getSheetView returns the SheetView object func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) { ws, err := f.workSheetReader(sheet) @@ -180,65 +36,100 @@ func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) return &(ws.SheetViews.SheetView[viewIndex]), err } -// SetSheetViewOptions sets sheet view options. The viewIndex may be negative -// and if so is counted backward (-1 is the last view). -// -// Available options: -// -// DefaultGridColor(bool) -// ShowFormulas(bool) -// ShowGridLines(bool) -// ShowRowColHeaders(bool) -// ShowZeros(bool) -// RightToLeft(bool) -// ShowRuler(bool) -// View(string) -// TopLeftCell(string) -// ZoomScale(float64) -// -// Example: -// -// err = f.SetSheetViewOptions("Sheet1", -1, ShowGridLines(false)) -func (f *File) SetSheetViewOptions(sheet string, viewIndex int, opts ...SheetViewOption) error { +// setSheetView set sheet view by given options. +func (view *xlsxSheetView) setSheetView(opts *ViewOptions) { + if opts.DefaultGridColor != nil { + view.DefaultGridColor = opts.DefaultGridColor + } + if opts.RightToLeft != nil { + view.RightToLeft = *opts.RightToLeft + } + if opts.ShowFormulas != nil { + view.ShowFormulas = *opts.ShowFormulas + } + if opts.ShowGridLines != nil { + view.ShowGridLines = opts.ShowGridLines + } + if opts.ShowRowColHeaders != nil { + view.ShowRowColHeaders = opts.ShowRowColHeaders + } + if opts.ShowRuler != nil { + view.ShowRuler = opts.ShowRuler + } + if opts.ShowZeros != nil { + view.ShowZeros = opts.ShowZeros + } + if opts.TopLeftCell != nil { + view.TopLeftCell = *opts.TopLeftCell + } + if opts.View != nil { + if _, ok := map[string]interface{}{ + "normal": nil, + "pageLayout": nil, + "pageBreakPreview": nil, + }[*opts.View]; ok { + view.View = *opts.View + } + } + if opts.ZoomScale != nil && *opts.ZoomScale >= 10 && *opts.ZoomScale <= 400 { + view.ZoomScale = *opts.ZoomScale + } +} + +// SetSheetView sets sheet view options. The viewIndex may be negative and if +// so is counted backward (-1 is the last view). +func (f *File) SetSheetView(sheet string, viewIndex int, opts *ViewOptions) error { view, err := f.getSheetView(sheet, viewIndex) if err != nil { return err } - - for _, opt := range opts { - opt.setSheetViewOption(view) + if opts == nil { + return err } + view.setSheetView(opts) return nil } -// GetSheetViewOptions gets the value of sheet view options. The viewIndex may -// be negative and if so is counted backward (-1 is the last view). -// -// Available options: -// -// DefaultGridColor(bool) -// ShowFormulas(bool) -// ShowGridLines(bool) -// ShowRowColHeaders(bool) -// ShowZeros(bool) -// RightToLeft(bool) -// ShowRuler(bool) -// View(string) -// TopLeftCell(string) -// ZoomScale(float64) -// -// Example: -// -// var showGridLines excelize.ShowGridLines -// err = f.GetSheetViewOptions("Sheet1", -1, &showGridLines) -func (f *File) GetSheetViewOptions(sheet string, viewIndex int, opts ...SheetViewOptionPtr) error { +// GetSheetView gets the value of sheet view options. The viewIndex may be +// negative and if so is counted backward (-1 is the last view). +func (f *File) GetSheetView(sheet string, viewIndex int) (ViewOptions, error) { + opts := ViewOptions{ + DefaultGridColor: boolPtr(true), + ShowFormulas: boolPtr(true), + ShowGridLines: boolPtr(true), + ShowRowColHeaders: boolPtr(true), + ShowRuler: boolPtr(true), + ShowZeros: boolPtr(true), + View: stringPtr("normal"), + ZoomScale: float64Ptr(100), + } view, err := f.getSheetView(sheet, viewIndex) if err != nil { - return err + return opts, err } - - for _, opt := range opts { - opt.getSheetViewOption(view) + if view.DefaultGridColor != nil { + opts.DefaultGridColor = view.DefaultGridColor } - return nil + opts.RightToLeft = boolPtr(view.RightToLeft) + opts.ShowFormulas = boolPtr(view.ShowFormulas) + if view.ShowGridLines != nil { + opts.ShowGridLines = view.ShowGridLines + } + if view.ShowRowColHeaders != nil { + opts.ShowRowColHeaders = view.ShowRowColHeaders + } + if view.ShowRuler != nil { + opts.ShowRuler = view.ShowRuler + } + if view.ShowZeros != nil { + opts.ShowZeros = view.ShowZeros + } + opts.TopLeftCell = stringPtr(view.TopLeftCell) + if view.View != "" { + opts.View = stringPtr(view.View) + } + if view.ZoomScale >= 10 && view.ZoomScale <= 400 { + opts.ZoomScale = float64Ptr(view.ZoomScale) + } + return opts, err } diff --git a/sheetview_test.go b/sheetview_test.go index 65c4f5120b..8d022a2e0f 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -1,218 +1,50 @@ package excelize import ( - "fmt" "testing" "github.com/stretchr/testify/assert" ) -var _ = []SheetViewOption{ - DefaultGridColor(true), - ShowFormulas(false), - ShowGridLines(true), - ShowRowColHeaders(true), - ShowZeros(true), - RightToLeft(false), - ShowRuler(false), - View("pageLayout"), - TopLeftCell("B2"), - ZoomScale(100), - // SheetViewOptionPtr are also SheetViewOption - new(DefaultGridColor), - new(ShowFormulas), - new(ShowGridLines), - new(ShowRowColHeaders), - new(ShowZeros), - new(RightToLeft), - new(ShowRuler), - new(View), - new(TopLeftCell), - new(ZoomScale), -} - -var _ = []SheetViewOptionPtr{ - (*DefaultGridColor)(nil), - (*ShowFormulas)(nil), - (*ShowGridLines)(nil), - (*ShowRowColHeaders)(nil), - (*ShowZeros)(nil), - (*RightToLeft)(nil), - (*ShowRuler)(nil), - (*View)(nil), - (*TopLeftCell)(nil), - (*ZoomScale)(nil), -} - -func ExampleFile_SetSheetViewOptions() { - f := NewFile() - const sheet = "Sheet1" - - if err := f.SetSheetViewOptions(sheet, 0, - DefaultGridColor(false), - ShowFormulas(true), - ShowGridLines(true), - ShowRowColHeaders(true), - RightToLeft(false), - ShowRuler(false), - View("pageLayout"), - TopLeftCell("C3"), - ZoomScale(80), - ); err != nil { - fmt.Println(err) - } - - var zoomScale ZoomScale - fmt.Println("Default:") - fmt.Println("- zoomScale: 80") - - if err := f.SetSheetViewOptions(sheet, 0, ZoomScale(500)); err != nil { - fmt.Println(err) - } - - if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - fmt.Println(err) - } - - fmt.Println("Used out of range value:") - fmt.Println("- zoomScale:", zoomScale) - - if err := f.SetSheetViewOptions(sheet, 0, ZoomScale(123)); err != nil { - fmt.Println(err) - } - - if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - fmt.Println(err) - } - - fmt.Println("Used correct value:") - fmt.Println("- zoomScale:", zoomScale) - - // Output: - // Default: - // - zoomScale: 80 - // Used out of range value: - // - zoomScale: 80 - // Used correct value: - // - zoomScale: 123 -} - -func ExampleFile_GetSheetViewOptions() { +func TestSetView(t *testing.T) { f := NewFile() - const sheet = "Sheet1" - - var ( - defaultGridColor DefaultGridColor - showFormulas ShowFormulas - showGridLines ShowGridLines - showRowColHeaders ShowRowColHeaders - showZeros ShowZeros - rightToLeft RightToLeft - showRuler ShowRuler - view View - topLeftCell TopLeftCell - zoomScale ZoomScale - ) - - if err := f.GetSheetViewOptions(sheet, 0, - &defaultGridColor, - &showFormulas, - &showGridLines, - &showRowColHeaders, - &showZeros, - &rightToLeft, - &showRuler, - &view, - &topLeftCell, - &zoomScale, - ); err != nil { - fmt.Println(err) - } - - fmt.Println("Default:") - fmt.Println("- defaultGridColor:", defaultGridColor) - fmt.Println("- showFormulas:", showFormulas) - fmt.Println("- showGridLines:", showGridLines) - fmt.Println("- showRowColHeaders:", showRowColHeaders) - fmt.Println("- showZeros:", showZeros) - fmt.Println("- rightToLeft:", rightToLeft) - fmt.Println("- showRuler:", showRuler) - fmt.Println("- view:", view) - fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`) - fmt.Println("- zoomScale:", zoomScale) - - if err := f.SetSheetViewOptions(sheet, 0, ShowGridLines(false)); err != nil { - fmt.Println(err) - } - - if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { - fmt.Println(err) - } - - if err := f.SetSheetViewOptions(sheet, 0, ShowZeros(false)); err != nil { - fmt.Println(err) - } - - if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil { - fmt.Println(err) - } - - if err := f.SetSheetViewOptions(sheet, 0, View("pageLayout")); err != nil { - fmt.Println(err) - } - - if err := f.GetSheetViewOptions(sheet, 0, &view); err != nil { - fmt.Println(err) - } - - if err := f.SetSheetViewOptions(sheet, 0, TopLeftCell("B2")); err != nil { - fmt.Println(err) - } - - if err := f.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { - fmt.Println(err) - } - - fmt.Println("After change:") - fmt.Println("- showGridLines:", showGridLines) - fmt.Println("- showZeros:", showZeros) - fmt.Println("- view:", view) - fmt.Println("- topLeftCell:", topLeftCell) - - // Output: - // Default: - // - defaultGridColor: true - // - showFormulas: false - // - showGridLines: true - // - showRowColHeaders: true - // - showZeros: true - // - rightToLeft: false - // - showRuler: true - // - view: normal - // - topLeftCell: "" - // - zoomScale: 0 - // After change: - // - showGridLines: false - // - showZeros: false - // - view: pageLayout - // - topLeftCell: B2 -} - -func TestSheetViewOptionsErrors(t *testing.T) { - f := NewFile() - const sheet = "Sheet1" - - assert.NoError(t, f.GetSheetViewOptions(sheet, 0)) - assert.NoError(t, f.GetSheetViewOptions(sheet, -1)) - assert.Error(t, f.GetSheetViewOptions(sheet, 1)) - assert.Error(t, f.GetSheetViewOptions(sheet, -2)) - assert.NoError(t, f.SetSheetViewOptions(sheet, 0)) - assert.NoError(t, f.SetSheetViewOptions(sheet, -1)) - assert.Error(t, f.SetSheetViewOptions(sheet, 1)) - assert.Error(t, f.SetSheetViewOptions(sheet, -2)) - + assert.NoError(t, f.SetSheetView("Sheet1", -1, nil)) ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).SheetViews = nil - assert.NoError(t, f.GetSheetViewOptions(sheet, 0)) + expected := ViewOptions{ + DefaultGridColor: boolPtr(false), + RightToLeft: boolPtr(false), + ShowFormulas: boolPtr(false), + ShowGridLines: boolPtr(false), + ShowRowColHeaders: boolPtr(false), + ShowRuler: boolPtr(false), + ShowZeros: boolPtr(false), + TopLeftCell: stringPtr("A1"), + View: stringPtr("normal"), + ZoomScale: float64Ptr(120), + } + assert.NoError(t, f.SetSheetView("Sheet1", 0, &expected)) + opts, err := f.GetSheetView("Sheet1", 0) + assert.NoError(t, err) + assert.Equal(t, expected, opts) + // Test set sheet view options with invalid view index. + assert.EqualError(t, f.SetSheetView("Sheet1", 1, nil), "view index 1 out of range") + assert.EqualError(t, f.SetSheetView("Sheet1", -2, nil), "view index -2 out of range") + // Test set sheet view options on not exists worksheet. + assert.EqualError(t, f.SetSheetView("SheetN", 0, nil), "sheet SheetN does not exist") +} + +func TestGetView(t *testing.T) { + f := NewFile() + _, err := f.getSheetView("SheetN", 0) + assert.EqualError(t, err, "sheet SheetN does not exist") + // Test get sheet view options with invalid view index. + _, err = f.GetSheetView("Sheet1", 1) + assert.EqualError(t, err, "view index 1 out of range") + _, err = f.GetSheetView("Sheet1", -2) + assert.EqualError(t, err, "view index -2 out of range") + // Test get sheet view options on not exists worksheet. + _, err = f.GetSheetView("SheetN", 0) + assert.EqualError(t, err, "sheet SheetN does not exist") } diff --git a/sparkline.go b/sparkline.go index 79cb1f2b71..f2e0d7a455 100644 --- a/sparkline.go +++ b/sparkline.go @@ -387,8 +387,9 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // Markers | Toggle sparkline markers // ColorAxis | An RGB Color is specified as RRGGBB // Axis | Show sparkline axis -func (f *File) AddSparkline(sheet string, opts *SparklineOption) (err error) { +func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error { var ( + err error ws *xlsxWorksheet sparkType string sparkTypes map[string]string @@ -401,7 +402,7 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOption) (err error) { // parameter validation if ws, err = f.parseFormatAddSparklineSet(sheet, opts); err != nil { - return + return err } // Handle the sparkline type sparkType = "line" @@ -409,7 +410,7 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOption) (err error) { if opts.Type != "" { if specifiedSparkTypes, ok = sparkTypes[opts.Type]; !ok { err = ErrSparklineType - return + return err } sparkType = specifiedSparkTypes } @@ -435,7 +436,7 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOption) (err error) { f.addSparkline(opts, group) if ws.ExtLst.Ext != "" { // append mode ext if err = f.appendSparkline(ws, group, groups); err != nil { - return + return err } } else { groups = &xlsxX14SparklineGroups{ @@ -443,23 +444,23 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOption) (err error) { SparklineGroups: []*xlsxX14SparklineGroup{group}, } if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { - return + return err } if extBytes, err = xml.Marshal(&xlsxWorksheetExt{ URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes), }); err != nil { - return + return err } ws.ExtLst.Ext = string(extBytes) } f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14) - return + return err } // parseFormatAddSparklineSet provides a function to validate sparkline // properties. -func (f *File) parseFormatAddSparklineSet(sheet string, opts *SparklineOption) (*xlsxWorksheet, error) { +func (f *File) parseFormatAddSparklineSet(sheet string, opts *SparklineOptions) (*xlsxWorksheet, error) { ws, err := f.workSheetReader(sheet) if err != nil { return ws, err @@ -488,7 +489,7 @@ func (f *File) parseFormatAddSparklineSet(sheet string, opts *SparklineOption) ( // addSparkline provides a function to create a sparkline in a sparkline group // by given properties. -func (f *File) addSparkline(opts *SparklineOption, group *xlsxX14SparklineGroup) { +func (f *File) addSparkline(opts *SparklineOptions, group *xlsxX14SparklineGroup) { for idx, location := range opts.Location { group.Sparklines.Sparkline = append(group.Sparklines.Sparkline, &xlsxX14Sparkline{ F: opts.Range[idx], @@ -499,8 +500,9 @@ func (f *File) addSparkline(opts *SparklineOption, group *xlsxX14SparklineGroup) // appendSparkline provides a function to append sparkline to sparkline // groups. -func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, groups *xlsxX14SparklineGroups) (err error) { +func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, groups *xlsxX14SparklineGroups) error { var ( + err error idx int decodeExtLst *decodeWorksheetExt decodeSparklineGroups *decodeX14SparklineGroups @@ -510,17 +512,17 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, decodeExtLst = new(decodeWorksheetExt) if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). Decode(decodeExtLst); err != nil && err != io.EOF { - return + return err } for idx, ext = range decodeExtLst.Ext { if ext.URI == ExtURISparklineGroups { decodeSparklineGroups = new(decodeX14SparklineGroups) if err = f.xmlNewDecoder(strings.NewReader(ext.Content)). Decode(decodeSparklineGroups); err != nil && err != io.EOF { - return + return err } if sparklineGroupBytes, err = xml.Marshal(group); err != nil { - return + return err } if groups == nil { groups = &xlsxX14SparklineGroups{} @@ -528,16 +530,16 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, groups.XMLNSXM = NameSpaceSpreadSheetExcel2006Main.Value groups.Content = decodeSparklineGroups.Content + string(sparklineGroupBytes) if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { - return + return err } decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes) } } if extLstBytes, err = xml.Marshal(decodeExtLst); err != nil { - return + return err } ws.ExtLst = &xlsxExtLst{ Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), ""), } - return + return err } diff --git a/sparkline_test.go b/sparkline_test.go index 4703c859d6..e20dfdc811 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -15,7 +15,10 @@ func TestAddSparkline(t *testing.T) { style, err := f.NewStyle(`{"font":{"bold":true}}`) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "B1", style)) - assert.NoError(t, f.SetSheetViewOptions("Sheet1", 0, ZoomScale(150))) + viewOpts, err := f.GetSheetView("Sheet1", 0) + assert.NoError(t, err) + viewOpts.ZoomScale = float64Ptr(150) + assert.NoError(t, f.SetSheetView("Sheet1", 0, &viewOpts)) assert.NoError(t, f.SetColWidth("Sheet1", "A", "A", 14)) assert.NoError(t, f.SetColWidth("Sheet1", "B", "B", 50)) @@ -24,34 +27,34 @@ func TestAddSparkline(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", "B1", "Description")) assert.NoError(t, f.SetCellValue("Sheet1", "B2", `A default "line" sparkline.`)) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A2"}, Range: []string{"Sheet3!A1:J1"}, })) assert.NoError(t, f.SetCellValue("Sheet1", "B3", `A default "column" sparkline.`)) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A3"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", })) assert.NoError(t, f.SetCellValue("Sheet1", "B4", `A default "win/loss" sparkline.`)) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A4"}, Range: []string{"Sheet3!A3:J3"}, Type: "win_loss", })) assert.NoError(t, f.SetCellValue("Sheet1", "B6", "Line with markers.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A6"}, Range: []string{"Sheet3!A1:J1"}, Markers: true, })) assert.NoError(t, f.SetCellValue("Sheet1", "B7", "Line with high and low points.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A7"}, Range: []string{"Sheet3!A1:J1"}, High: true, @@ -59,7 +62,7 @@ func TestAddSparkline(t *testing.T) { })) assert.NoError(t, f.SetCellValue("Sheet1", "B8", "Line with first and last point markers.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A8"}, Range: []string{"Sheet3!A1:J1"}, First: true, @@ -67,28 +70,28 @@ func TestAddSparkline(t *testing.T) { })) assert.NoError(t, f.SetCellValue("Sheet1", "B9", "Line with negative point markers.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A9"}, Range: []string{"Sheet3!A1:J1"}, Negative: true, })) assert.NoError(t, f.SetCellValue("Sheet1", "B10", "Line with axis.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A10"}, Range: []string{"Sheet3!A1:J1"}, Axis: true, })) assert.NoError(t, f.SetCellValue("Sheet1", "B12", "Column with default style (1).")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A12"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", })) assert.NoError(t, f.SetCellValue("Sheet1", "B13", "Column with style 2.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A13"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", @@ -96,7 +99,7 @@ func TestAddSparkline(t *testing.T) { })) assert.NoError(t, f.SetCellValue("Sheet1", "B14", "Column with style 3.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A14"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", @@ -104,7 +107,7 @@ func TestAddSparkline(t *testing.T) { })) assert.NoError(t, f.SetCellValue("Sheet1", "B15", "Column with style 4.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A15"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", @@ -112,7 +115,7 @@ func TestAddSparkline(t *testing.T) { })) assert.NoError(t, f.SetCellValue("Sheet1", "B16", "Column with style 5.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A16"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", @@ -120,7 +123,7 @@ func TestAddSparkline(t *testing.T) { })) assert.NoError(t, f.SetCellValue("Sheet1", "B17", "Column with style 6.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A17"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", @@ -128,7 +131,7 @@ func TestAddSparkline(t *testing.T) { })) assert.NoError(t, f.SetCellValue("Sheet1", "B18", "Column with a user defined color.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A18"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", @@ -136,14 +139,14 @@ func TestAddSparkline(t *testing.T) { })) assert.NoError(t, f.SetCellValue("Sheet1", "B20", "A win/loss sparkline.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A20"}, Range: []string{"Sheet3!A3:J3"}, Type: "win_loss", })) assert.NoError(t, f.SetCellValue("Sheet1", "B21", "A win/loss sparkline with negative points highlighted.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A21"}, Range: []string{"Sheet3!A3:J3"}, Type: "win_loss", @@ -151,7 +154,7 @@ func TestAddSparkline(t *testing.T) { })) assert.NoError(t, f.SetCellValue("Sheet1", "B23", "A left to right column (the default).")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A23"}, Range: []string{"Sheet3!A4:J4"}, Type: "column", @@ -159,7 +162,7 @@ func TestAddSparkline(t *testing.T) { })) assert.NoError(t, f.SetCellValue("Sheet1", "B24", "A right to left column.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A24"}, Range: []string{"Sheet3!A4:J4"}, Type: "column", @@ -168,7 +171,7 @@ func TestAddSparkline(t *testing.T) { })) assert.NoError(t, f.SetCellValue("Sheet1", "B25", "Sparkline and text in one cell.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A25"}, Range: []string{"Sheet3!A4:J4"}, Type: "column", @@ -177,34 +180,34 @@ func TestAddSparkline(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", "A25", "Growth")) assert.NoError(t, f.SetCellValue("Sheet1", "B27", "A grouped sparkline. Changes are applied to all three.")) - assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A27", "A28", "A29"}, Range: []string{"Sheet3!A5:J5", "Sheet3!A6:J6", "Sheet3!A7:J7"}, Markers: true, })) // Sheet2 sections - assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Type: "win_loss", Negative: true, })) - assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOptions{ Location: []string{"F1"}, Range: []string{"Sheet2!A1:E1"}, Markers: true, })) - assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOptions{ Location: []string{"F2"}, Range: []string{"Sheet2!A2:E2"}, Type: "column", Style: 12, })) - assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{ + assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Type: "win_loss", @@ -215,39 +218,39 @@ func TestAddSparkline(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddSparkline.xlsx"))) // Test error exceptions - assert.EqualError(t, f.AddSparkline("SheetN", &SparklineOption{ + assert.EqualError(t, f.AddSparkline("SheetN", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, }), "sheet SheetN does not exist") assert.EqualError(t, f.AddSparkline("Sheet1", nil), ErrParameterRequired.Error()) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Range: []string{"Sheet2!A3:E3"}, }), ErrSparklineLocation.Error()) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, }), ErrSparklineRange.Error()) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F2", "F3"}, Range: []string{"Sheet2!A3:E3"}, }), ErrSparkline.Error()) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Type: "unknown_type", }), ErrSparklineType.Error()) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Style: -1, }), ErrSparklineStyle.Error()) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Style: -1, @@ -265,7 +268,7 @@ func TestAddSparkline(t *testing.T) { ` - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A2"}, Range: []string{"Sheet3!A1:J1"}, }), "XML syntax error on line 6: element closed by ") diff --git a/stream.go b/stream.go index 019731f92c..3d06790276 100644 --- a/stream.go +++ b/stream.go @@ -139,8 +139,8 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { // called after the rows are written but before Flush. // // See File.AddTable for details on the table format. -func (sw *StreamWriter) AddTable(hCell, vCell, format string) error { - formatSet, err := parseFormatTableSet(format) +func (sw *StreamWriter) AddTable(hCell, vCell, opts string) error { + options, err := parseTableOptions(opts) if err != nil { return err } @@ -177,7 +177,7 @@ func (sw *StreamWriter) AddTable(hCell, vCell, format string) error { tableID := sw.File.countTables() + 1 - name := formatSet.TableName + name := options.TableName if name == "" { name = "Table" + strconv.Itoa(tableID) } @@ -196,11 +196,11 @@ func (sw *StreamWriter) AddTable(hCell, vCell, format string) error { TableColumn: tableColumn, }, TableStyleInfo: &xlsxTableStyleInfo{ - Name: formatSet.TableStyle, - ShowFirstColumn: formatSet.ShowFirstColumn, - ShowLastColumn: formatSet.ShowLastColumn, - ShowRowStripes: formatSet.ShowRowStripes, - ShowColumnStripes: formatSet.ShowColumnStripes, + Name: options.TableStyle, + ShowFirstColumn: options.ShowFirstColumn, + ShowLastColumn: options.ShowLastColumn, + ShowRowStripes: options.ShowRowStripes, + ShowColumnStripes: options.ShowColumnStripes, }, } diff --git a/stream_test.go b/stream_test.go index 1026cb3492..80875c79a6 100644 --- a/stream_test.go +++ b/stream_test.go @@ -173,7 +173,7 @@ func TestStreamTable(t *testing.T) { assert.NoError(t, streamWriter.AddTable("A1", "C1", ``)) - // Test add table with illegal formatset. + // Test add table with illegal options. assert.EqualError(t, streamWriter.AddTable("B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") // Test add table with illegal cell reference. assert.EqualError(t, streamWriter.AddTable("A", "B1", `{}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) diff --git a/styles.go b/styles.go index a4f5dc48dc..6d90a9ec3a 100644 --- a/styles.go +++ b/styles.go @@ -2859,13 +2859,13 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // max_color - Same as min_color, see above. // // bar_color - Used for data_bar. Same as min_color, see above. -func (f *File) SetConditionalFormat(sheet, reference, formatSet string) error { - var format []*formatConditional - err := json.Unmarshal([]byte(formatSet), &format) +func (f *File) SetConditionalFormat(sheet, reference, opts string) error { + var format []*conditionalOptions + err := json.Unmarshal([]byte(opts), &format) if err != nil { return err } - drawContFmtFunc := map[string]func(p int, ct string, fmtCond *formatConditional) *xlsxCfRule{ + drawContFmtFunc := map[string]func(p int, ct string, fmtCond *conditionalOptions) *xlsxCfRule{ "cellIs": drawCondFmtCellIs, "top10": drawCondFmtTop10, "aboveAverage": drawCondFmtAboveAverage, @@ -2909,8 +2909,8 @@ func (f *File) SetConditionalFormat(sheet, reference, formatSet string) error { // extractCondFmtCellIs provides a function to extract conditional format // settings for cell value (include between, not between, equal, not equal, // greater than and less than) by given conditional formatting rule. -func extractCondFmtCellIs(c *xlsxCfRule) *formatConditional { - format := formatConditional{Type: "cell", Criteria: operatorType[c.Operator], Format: *c.DxfID} +func extractCondFmtCellIs(c *xlsxCfRule) *conditionalOptions { + format := conditionalOptions{Type: "cell", Criteria: operatorType[c.Operator], Format: *c.DxfID} if len(c.Formula) == 2 { format.Minimum, format.Maximum = c.Formula[0], c.Formula[1] return &format @@ -2922,8 +2922,8 @@ func extractCondFmtCellIs(c *xlsxCfRule) *formatConditional { // extractCondFmtTop10 provides a function to extract conditional format // settings for top N (default is top 10) by given conditional formatting // rule. -func extractCondFmtTop10(c *xlsxCfRule) *formatConditional { - format := formatConditional{ +func extractCondFmtTop10(c *xlsxCfRule) *conditionalOptions { + format := conditionalOptions{ Type: "top", Criteria: "=", Format: *c.DxfID, @@ -2939,8 +2939,8 @@ func extractCondFmtTop10(c *xlsxCfRule) *formatConditional { // extractCondFmtAboveAverage provides a function to extract conditional format // settings for above average and below average by given conditional formatting // rule. -func extractCondFmtAboveAverage(c *xlsxCfRule) *formatConditional { - return &formatConditional{ +func extractCondFmtAboveAverage(c *xlsxCfRule) *conditionalOptions { + return &conditionalOptions{ Type: "average", Criteria: "=", Format: *c.DxfID, @@ -2951,8 +2951,8 @@ func extractCondFmtAboveAverage(c *xlsxCfRule) *formatConditional { // extractCondFmtDuplicateUniqueValues provides a function to extract // conditional format settings for duplicate and unique values by given // conditional formatting rule. -func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule) *formatConditional { - return &formatConditional{ +func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule) *conditionalOptions { + return &conditionalOptions{ Type: map[string]string{ "duplicateValues": "duplicate", "uniqueValues": "unique", @@ -2965,8 +2965,8 @@ func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule) *formatConditional { // extractCondFmtColorScale provides a function to extract conditional format // settings for color scale (include 2 color scale and 3 color scale) by given // conditional formatting rule. -func extractCondFmtColorScale(c *xlsxCfRule) *formatConditional { - var format formatConditional +func extractCondFmtColorScale(c *xlsxCfRule) *conditionalOptions { + var format conditionalOptions format.Type, format.Criteria = "2_color_scale", "=" values := len(c.ColorScale.Cfvo) colors := len(c.ColorScale.Color) @@ -3000,8 +3000,8 @@ func extractCondFmtColorScale(c *xlsxCfRule) *formatConditional { // extractCondFmtDataBar provides a function to extract conditional format // settings for data bar by given conditional formatting rule. -func extractCondFmtDataBar(c *xlsxCfRule) *formatConditional { - format := formatConditional{Type: "data_bar", Criteria: "="} +func extractCondFmtDataBar(c *xlsxCfRule) *conditionalOptions { + format := conditionalOptions{Type: "data_bar", Criteria: "="} if c.DataBar != nil { format.MinType = c.DataBar.Cfvo[0].Type format.MaxType = c.DataBar.Cfvo[1].Type @@ -3012,8 +3012,8 @@ func extractCondFmtDataBar(c *xlsxCfRule) *formatConditional { // extractCondFmtExp provides a function to extract conditional format settings // for expression by given conditional formatting rule. -func extractCondFmtExp(c *xlsxCfRule) *formatConditional { - format := formatConditional{Type: "formula", Format: *c.DxfID} +func extractCondFmtExp(c *xlsxCfRule) *conditionalOptions { + format := conditionalOptions{Type: "formula", Format: *c.DxfID} if len(c.Formula) > 0 { format.Criteria = c.Formula[0] } @@ -3023,7 +3023,7 @@ func extractCondFmtExp(c *xlsxCfRule) *formatConditional { // GetConditionalFormats returns conditional format settings by given worksheet // name. func (f *File) GetConditionalFormats(sheet string) (map[string]string, error) { - extractContFmtFunc := map[string]func(c *xlsxCfRule) *formatConditional{ + extractContFmtFunc := map[string]func(c *xlsxCfRule) *conditionalOptions{ "cellIs": extractCondFmtCellIs, "top10": extractCondFmtTop10, "aboveAverage": extractCondFmtAboveAverage, @@ -3040,14 +3040,14 @@ func (f *File) GetConditionalFormats(sheet string) (map[string]string, error) { return conditionalFormats, err } for _, cf := range ws.ConditionalFormatting { - var format []*formatConditional + var opts []*conditionalOptions for _, cr := range cf.CfRule { if extractFunc, ok := extractContFmtFunc[cr.Type]; ok { - format = append(format, extractFunc(cr)) + opts = append(opts, extractFunc(cr)) } } - formatSet, _ := json.Marshal(format) - conditionalFormats[cf.SQRef] = string(formatSet) + options, _ := json.Marshal(opts) + conditionalFormats[cf.SQRef] = string(options) } return conditionalFormats, err } @@ -3071,7 +3071,7 @@ func (f *File) UnsetConditionalFormat(sheet, reference string) error { // drawCondFmtCellIs provides a function to create conditional formatting rule // for cell value (include between, not between, equal, not equal, greater // than and less than) by given priority, criteria type and format settings. -func drawCondFmtCellIs(p int, ct string, format *formatConditional) *xlsxCfRule { +func drawCondFmtCellIs(p int, ct string, format *conditionalOptions) *xlsxCfRule { c := &xlsxCfRule{ Priority: p + 1, Type: validType[format.Type], @@ -3094,7 +3094,7 @@ func drawCondFmtCellIs(p int, ct string, format *formatConditional) *xlsxCfRule // drawCondFmtTop10 provides a function to create conditional formatting rule // for top N (default is top 10) by given priority, criteria type and format // settings. -func drawCondFmtTop10(p int, ct string, format *formatConditional) *xlsxCfRule { +func drawCondFmtTop10(p int, ct string, format *conditionalOptions) *xlsxCfRule { c := &xlsxCfRule{ Priority: p + 1, Bottom: format.Type == "bottom", @@ -3113,7 +3113,7 @@ func drawCondFmtTop10(p int, ct string, format *formatConditional) *xlsxCfRule { // drawCondFmtAboveAverage provides a function to create conditional // formatting rule for above average and below average by given priority, // criteria type and format settings. -func drawCondFmtAboveAverage(p int, ct string, format *formatConditional) *xlsxCfRule { +func drawCondFmtAboveAverage(p int, ct string, format *conditionalOptions) *xlsxCfRule { return &xlsxCfRule{ Priority: p + 1, Type: validType[format.Type], @@ -3125,7 +3125,7 @@ func drawCondFmtAboveAverage(p int, ct string, format *formatConditional) *xlsxC // drawCondFmtDuplicateUniqueValues provides a function to create conditional // formatting rule for duplicate and unique values by given priority, criteria // type and format settings. -func drawCondFmtDuplicateUniqueValues(p int, ct string, format *formatConditional) *xlsxCfRule { +func drawCondFmtDuplicateUniqueValues(p int, ct string, format *conditionalOptions) *xlsxCfRule { return &xlsxCfRule{ Priority: p + 1, Type: validType[format.Type], @@ -3136,7 +3136,7 @@ func drawCondFmtDuplicateUniqueValues(p int, ct string, format *formatConditiona // drawCondFmtColorScale provides a function to create conditional formatting // rule for color scale (include 2 color scale and 3 color scale) by given // priority, criteria type and format settings. -func drawCondFmtColorScale(p int, ct string, format *formatConditional) *xlsxCfRule { +func drawCondFmtColorScale(p int, ct string, format *conditionalOptions) *xlsxCfRule { minValue := format.MinValue if minValue == "" { minValue = "0" @@ -3173,7 +3173,7 @@ func drawCondFmtColorScale(p int, ct string, format *formatConditional) *xlsxCfR // drawCondFmtDataBar provides a function to create conditional formatting // rule for data bar by given priority, criteria type and format settings. -func drawCondFmtDataBar(p int, ct string, format *formatConditional) *xlsxCfRule { +func drawCondFmtDataBar(p int, ct string, format *conditionalOptions) *xlsxCfRule { return &xlsxCfRule{ Priority: p + 1, Type: validType[format.Type], @@ -3186,7 +3186,7 @@ func drawCondFmtDataBar(p int, ct string, format *formatConditional) *xlsxCfRule // drawCondFmtExp provides a function to create conditional formatting rule // for expression by given priority, criteria type and format settings. -func drawCondFmtExp(p int, ct string, format *formatConditional) *xlsxCfRule { +func drawCondFmtExp(p int, ct string, format *conditionalOptions) *xlsxCfRule { return &xlsxCfRule{ Priority: p + 1, Type: validType[format.Type], diff --git a/styles_test.go b/styles_test.go index 47aee5b802..f27c9a20e2 100644 --- a/styles_test.go +++ b/styles_test.go @@ -192,9 +192,9 @@ func TestGetConditionalFormats(t *testing.T) { f := NewFile() err := f.SetConditionalFormat("Sheet1", "A1:A2", format) assert.NoError(t, err) - formatSet, err := f.GetConditionalFormats("Sheet1") + opts, err := f.GetConditionalFormats("Sheet1") assert.NoError(t, err) - assert.Equal(t, format, formatSet["A1:A2"]) + assert.Equal(t, format, opts["A1:A2"]) } // Test get conditional formats on no exists worksheet f := NewFile() diff --git a/table.go b/table.go index 7ef75625b3..f7cac2097a 100644 --- a/table.go +++ b/table.go @@ -20,12 +20,12 @@ import ( "strings" ) -// parseFormatTableSet provides a function to parse the format settings of the +// parseTableOptions provides a function to parse the format settings of the // table with default value. -func parseFormatTableSet(formatSet string) (*formatTable, error) { - format := formatTable{ShowRowStripes: true} - err := json.Unmarshal(parseFormatSet(formatSet), &format) - return &format, err +func parseTableOptions(opts string) (*tableOptions, error) { + options := tableOptions{ShowRowStripes: true} + err := json.Unmarshal(fallbackOptions(opts), &options) + return &options, err } // AddTable provides the method to add table in a worksheet by given worksheet @@ -57,8 +57,8 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) { // TableStyleLight1 - TableStyleLight21 // TableStyleMedium1 - TableStyleMedium28 // TableStyleDark1 - TableStyleDark11 -func (f *File) AddTable(sheet, hCell, vCell, format string) error { - formatSet, err := parseFormatTableSet(format) +func (f *File) AddTable(sheet, hCell, vCell, opts string) error { + options, err := parseTableOptions(opts) if err != nil { return err } @@ -91,7 +91,7 @@ func (f *File) AddTable(sheet, hCell, vCell, format string) error { return err } f.addSheetNameSpace(sheet, SourceRelationship) - if err = f.addTable(sheet, tableXML, hCol, hRow, vCol, vRow, tableID, formatSet); err != nil { + if err = f.addTable(sheet, tableXML, hCol, hRow, vCol, vRow, tableID, options); err != nil { return err } f.addContentTypePart(tableID, "table") @@ -160,7 +160,7 @@ func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn, // addTable provides a function to add table by given worksheet name, // range reference and format set. -func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet *formatTable) error { +func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *tableOptions) error { // Correct the minimum number of rows, the table at least two lines. if y1 == y2 { y2++ @@ -172,7 +172,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet return err } tableColumns, _ := f.setTableHeader(sheet, x1, y1, x2) - name := formatSet.TableName + name := opts.TableName if name == "" { name = "Table" + strconv.Itoa(i) } @@ -190,11 +190,11 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet TableColumn: tableColumns, }, TableStyleInfo: &xlsxTableStyleInfo{ - Name: formatSet.TableStyle, - ShowFirstColumn: formatSet.ShowFirstColumn, - ShowLastColumn: formatSet.ShowLastColumn, - ShowRowStripes: formatSet.ShowRowStripes, - ShowColumnStripes: formatSet.ShowColumnStripes, + Name: opts.TableStyle, + ShowFirstColumn: opts.ShowFirstColumn, + ShowLastColumn: opts.ShowLastColumn, + ShowRowStripes: opts.ShowRowStripes, + ShowColumnStripes: opts.ShowColumnStripes, }, } table, _ := xml.Marshal(t) @@ -202,12 +202,12 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet return nil } -// parseAutoFilterSet provides a function to parse the settings of the auto +// parseAutoFilterOptions provides a function to parse the settings of the auto // filter. -func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { - format := formatAutoFilter{} - err := json.Unmarshal([]byte(formatSet), &format) - return &format, err +func parseAutoFilterOptions(opts string) (*autoFilterOptions, error) { + options := autoFilterOptions{} + err := json.Unmarshal([]byte(opts), &options) + return &options, err } // AutoFilter provides the method to add auto filter in a worksheet by given @@ -279,7 +279,7 @@ func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { // x < 2000 // col < 2000 // Price < 2000 -func (f *File) AutoFilter(sheet, hCell, vCell, format string) error { +func (f *File) AutoFilter(sheet, hCell, vCell, opts string) error { hCol, hRow, err := CellNameToCoordinates(hCell) if err != nil { return err @@ -297,7 +297,7 @@ func (f *File) AutoFilter(sheet, hCell, vCell, format string) error { vRow, hRow = hRow, vRow } - formatSet, _ := parseAutoFilterSet(format) + options, _ := parseAutoFilterOptions(opts) cellStart, _ := CoordinatesToCellName(hCol, hRow, true) cellEnd, _ := CoordinatesToCellName(vCol, vRow, true) ref, filterDB := cellStart+":"+cellEnd, "_xlnm._FilterDatabase" @@ -328,12 +328,12 @@ func (f *File) AutoFilter(sheet, hCell, vCell, format string) error { } } refRange := vCol - hCol - return f.autoFilter(sheet, ref, refRange, hCol, formatSet) + return f.autoFilter(sheet, ref, refRange, hCol, options) } // autoFilter provides a function to extract the tokens from the filter // expression. The tokens are mainly non-whitespace groups. -func (f *File) autoFilter(sheet, ref string, refRange, col int, formatSet *formatAutoFilter) error { +func (f *File) autoFilter(sheet, ref string, refRange, col int, opts *autoFilterOptions) error { ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -346,28 +346,28 @@ func (f *File) autoFilter(sheet, ref string, refRange, col int, formatSet *forma Ref: ref, } ws.AutoFilter = filter - if formatSet.Column == "" || formatSet.Expression == "" { + if opts.Column == "" || opts.Expression == "" { return nil } - fsCol, err := ColumnNameToNumber(formatSet.Column) + fsCol, err := ColumnNameToNumber(opts.Column) if err != nil { return err } offset := fsCol - col if offset < 0 || offset > refRange { - return fmt.Errorf("incorrect index of column '%s'", formatSet.Column) + return fmt.Errorf("incorrect index of column '%s'", opts.Column) } filter.FilterColumn = append(filter.FilterColumn, &xlsxFilterColumn{ ColID: offset, }) re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`) - token := re.FindAllString(formatSet.Expression, -1) + token := re.FindAllString(opts.Expression, -1) if len(token) != 3 && len(token) != 7 { - return fmt.Errorf("incorrect number of tokens in criteria '%s'", formatSet.Expression) + return fmt.Errorf("incorrect number of tokens in criteria '%s'", opts.Expression) } - expressions, tokens, err := f.parseFilterExpression(formatSet.Expression, token) + expressions, tokens, err := f.parseFilterExpression(opts.Expression, token) if err != nil { return err } diff --git a/table_test.go b/table_test.go index c997ad243b..409b49fb5d 100644 --- a/table_test.go +++ b/table_test.go @@ -31,7 +31,7 @@ func TestAddTable(t *testing.T) { // Test add table in not exist worksheet. assert.EqualError(t, f.AddTable("SheetN", "B26", "A21", `{}`), "sheet SheetN does not exist") - // Test add table with illegal formatset. + // Test add table with illegal options. assert.EqualError(t, f.AddTable("Sheet1", "B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") // Test add table with illegal cell reference. assert.EqualError(t, f.AddTable("Sheet1", "A", "B1", `{}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) @@ -108,19 +108,19 @@ func TestAutoFilterError(t *testing.T) { }) } - assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, &formatAutoFilter{ + assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, &autoFilterOptions{ Column: "A", Expression: "", }), "sheet SheetN does not exist") - assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &formatAutoFilter{ + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &autoFilterOptions{ Column: "-", Expression: "-", }), newInvalidColumnNameError("-").Error()) - assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, &formatAutoFilter{ + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, &autoFilterOptions{ Column: "A", Expression: "-", }), `incorrect index of column 'A'`) - assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &formatAutoFilter{ + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &autoFilterOptions{ Column: "A", Expression: "-", }), `incorrect number of tokens in criteria '-'`) diff --git a/workbook.go b/workbook.go index dbe212acca..937c9caf4e 100644 --- a/workbook.go +++ b/workbook.go @@ -21,26 +21,38 @@ import ( "strings" ) -// WorkbookPrOption is an option of a view of a workbook. See SetWorkbookPrOptions(). -type WorkbookPrOption interface { - setWorkbookPrOption(pr *xlsxWorkbookPr) +// SetWorkbookProps provides a function to sets workbook properties. +func (f *File) SetWorkbookProps(opts *WorkbookPropsOptions) error { + wb := f.workbookReader() + if wb.WorkbookPr == nil { + wb.WorkbookPr = new(xlsxWorkbookPr) + } + if opts == nil { + return nil + } + if opts.Date1904 != nil { + wb.WorkbookPr.Date1904 = *opts.Date1904 + } + if opts.FilterPrivacy != nil { + wb.WorkbookPr.FilterPrivacy = *opts.FilterPrivacy + } + if opts.CodeName != nil { + wb.WorkbookPr.CodeName = *opts.CodeName + } + return nil } -// WorkbookPrOptionPtr is a writable WorkbookPrOption. See GetWorkbookPrOptions(). -type WorkbookPrOptionPtr interface { - WorkbookPrOption - getWorkbookPrOption(pr *xlsxWorkbookPr) +// GetWorkbookProps provides a function to gets workbook properties. +func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error) { + wb, opts := f.workbookReader(), WorkbookPropsOptions{} + if wb.WorkbookPr != nil { + opts.Date1904 = boolPtr(wb.WorkbookPr.Date1904) + opts.FilterPrivacy = boolPtr(wb.WorkbookPr.FilterPrivacy) + opts.CodeName = stringPtr(wb.WorkbookPr.CodeName) + } + return opts, nil } -type ( - // Date1904 is an option used for WorkbookPrOption, that indicates whether - // to use a 1900 or 1904 date system when converting serial date-times in - // the workbook to dates - Date1904 bool - // FilterPrivacy is an option used for WorkbookPrOption - FilterPrivacy bool -) - // setWorkbook update workbook property of the spreadsheet. Maximum 31 // characters are allowed in sheet title. func (f *File) setWorkbook(name string, sheetID, rid int) { @@ -116,84 +128,3 @@ func (f *File) workBookWriter() { f.saveFileList(f.getWorkbookPath(), replaceRelationshipsBytes(f.replaceNameSpaceBytes(f.getWorkbookPath(), output))) } } - -// SetWorkbookPrOptions provides a function to sets workbook properties. -// -// Available options: -// -// Date1904(bool) -// FilterPrivacy(bool) -// CodeName(string) -func (f *File) SetWorkbookPrOptions(opts ...WorkbookPrOption) error { - wb := f.workbookReader() - pr := wb.WorkbookPr - if pr == nil { - pr = new(xlsxWorkbookPr) - wb.WorkbookPr = pr - } - for _, opt := range opts { - opt.setWorkbookPrOption(pr) - } - return nil -} - -// setWorkbookPrOption implements the WorkbookPrOption interface. -func (o Date1904) setWorkbookPrOption(pr *xlsxWorkbookPr) { - pr.Date1904 = bool(o) -} - -// setWorkbookPrOption implements the WorkbookPrOption interface. -func (o FilterPrivacy) setWorkbookPrOption(pr *xlsxWorkbookPr) { - pr.FilterPrivacy = bool(o) -} - -// setWorkbookPrOption implements the WorkbookPrOption interface. -func (o CodeName) setWorkbookPrOption(pr *xlsxWorkbookPr) { - pr.CodeName = string(o) -} - -// GetWorkbookPrOptions provides a function to gets workbook properties. -// -// Available options: -// -// Date1904(bool) -// FilterPrivacy(bool) -// CodeName(string) -func (f *File) GetWorkbookPrOptions(opts ...WorkbookPrOptionPtr) error { - wb := f.workbookReader() - pr := wb.WorkbookPr - for _, opt := range opts { - opt.getWorkbookPrOption(pr) - } - return nil -} - -// getWorkbookPrOption implements the WorkbookPrOption interface and get the -// date1904 of the workbook. -func (o *Date1904) getWorkbookPrOption(pr *xlsxWorkbookPr) { - if pr == nil { - *o = false - return - } - *o = Date1904(pr.Date1904) -} - -// getWorkbookPrOption implements the WorkbookPrOption interface and get the -// filter privacy of the workbook. -func (o *FilterPrivacy) getWorkbookPrOption(pr *xlsxWorkbookPr) { - if pr == nil { - *o = false - return - } - *o = FilterPrivacy(pr.FilterPrivacy) -} - -// getWorkbookPrOption implements the WorkbookPrOption interface and get the -// code name of the workbook. -func (o *CodeName) getWorkbookPrOption(pr *xlsxWorkbookPr) { - if pr == nil { - *o = "" - return - } - *o = CodeName(pr.CodeName) -} diff --git a/workbook_test.go b/workbook_test.go index 18b222c00f..29571fab45 100644 --- a/workbook_test.go +++ b/workbook_test.go @@ -1,69 +1,23 @@ package excelize import ( - "fmt" "testing" "github.com/stretchr/testify/assert" ) -func ExampleFile_SetWorkbookPrOptions() { - f := NewFile() - if err := f.SetWorkbookPrOptions( - Date1904(false), - FilterPrivacy(false), - CodeName("code"), - ); err != nil { - fmt.Println(err) - } - // Output: -} - -func ExampleFile_GetWorkbookPrOptions() { - f := NewFile() - var ( - date1904 Date1904 - filterPrivacy FilterPrivacy - codeName CodeName - ) - if err := f.GetWorkbookPrOptions(&date1904); err != nil { - fmt.Println(err) - } - if err := f.GetWorkbookPrOptions(&filterPrivacy); err != nil { - fmt.Println(err) - } - if err := f.GetWorkbookPrOptions(&codeName); err != nil { - fmt.Println(err) - } - fmt.Println("Defaults:") - fmt.Printf("- date1904: %t\n", date1904) - fmt.Printf("- filterPrivacy: %t\n", filterPrivacy) - fmt.Printf("- codeName: %q\n", codeName) - // Output: - // Defaults: - // - date1904: false - // - filterPrivacy: true - // - codeName: "" -} - -func TestWorkbookPr(t *testing.T) { +func TestWorkbookProps(t *testing.T) { f := NewFile() + assert.NoError(t, f.SetWorkbookProps(nil)) wb := f.workbookReader() wb.WorkbookPr = nil - var date1904 Date1904 - assert.NoError(t, f.GetWorkbookPrOptions(&date1904)) - assert.Equal(t, false, bool(date1904)) - - wb.WorkbookPr = nil - var codeName CodeName - assert.NoError(t, f.GetWorkbookPrOptions(&codeName)) - assert.Equal(t, "", string(codeName)) - assert.NoError(t, f.SetWorkbookPrOptions(CodeName("code"))) - assert.NoError(t, f.GetWorkbookPrOptions(&codeName)) - assert.Equal(t, "code", string(codeName)) - - wb.WorkbookPr = nil - var filterPrivacy FilterPrivacy - assert.NoError(t, f.GetWorkbookPrOptions(&filterPrivacy)) - assert.Equal(t, false, bool(filterPrivacy)) + expected := WorkbookPropsOptions{ + Date1904: boolPtr(true), + FilterPrivacy: boolPtr(true), + CodeName: stringPtr("code"), + } + assert.NoError(t, f.SetWorkbookProps(&expected)) + opts, err := f.GetWorkbookProps() + assert.NoError(t, err) + assert.Equal(t, expected, opts) } diff --git a/xmlChart.go b/xmlChart.go index dcd33e4a4b..53755f37ad 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -518,8 +518,8 @@ type cPageMargins struct { T float64 `xml:"t,attr"` } -// formatChartAxis directly maps the format settings of the chart axis. -type formatChartAxis struct { +// chartAxisOptions directly maps the format settings of the chart axis. +type chartAxisOptions struct { None bool `json:"none"` Crossing string `json:"crossing"` MajorGridlines bool `json:"major_grid_lines"` @@ -543,26 +543,27 @@ type formatChartAxis struct { Italic bool `json:"italic"` Underline bool `json:"underline"` } `json:"num_font"` - LogBase float64 `json:"logbase"` - NameLayout formatLayout `json:"name_layout"` + LogBase float64 `json:"logbase"` + NameLayout layoutOptions `json:"name_layout"` } -type formatChartDimension struct { +// chartDimensionOptions directly maps the dimension of the chart. +type chartDimensionOptions struct { Width int `json:"width"` Height int `json:"height"` } -// formatChart directly maps the format settings of the chart. -type formatChart struct { - Type string `json:"type"` - Series []formatChartSeries `json:"series"` - Format formatPicture `json:"format"` - Dimension formatChartDimension `json:"dimension"` - Legend formatChartLegend `json:"legend"` - Title formatChartTitle `json:"title"` - VaryColors bool `json:"vary_colors"` - XAxis formatChartAxis `json:"x_axis"` - YAxis formatChartAxis `json:"y_axis"` +// chartOptions directly maps the format settings of the chart. +type chartOptions struct { + Type string `json:"type"` + Series []chartSeriesOptions `json:"series"` + Format pictureOptions `json:"format"` + Dimension chartDimensionOptions `json:"dimension"` + Legend chartLegendOptions `json:"legend"` + Title chartTitleOptions `json:"title"` + VaryColors bool `json:"vary_colors"` + XAxis chartAxisOptions `json:"x_axis"` + YAxis chartAxisOptions `json:"y_axis"` Chartarea struct { Border struct { None bool `json:"none"` @@ -594,7 +595,7 @@ type formatChart struct { Fill struct { Color string `json:"color"` } `json:"fill"` - Layout formatLayout `json:"layout"` + Layout layoutOptions `json:"layout"` } `json:"plotarea"` ShowBlanksAs string `json:"show_blanks_as"` ShowHiddenData bool `json:"show_hidden_data"` @@ -603,19 +604,19 @@ type formatChart struct { order int } -// formatChartLegend directly maps the format settings of the chart legend. -type formatChartLegend struct { - None bool `json:"none"` - DeleteSeries []int `json:"delete_series"` - Font Font `json:"font"` - Layout formatLayout `json:"layout"` - Position string `json:"position"` - ShowLegendEntry bool `json:"show_legend_entry"` - ShowLegendKey bool `json:"show_legend_key"` +// chartLegendOptions directly maps the format settings of the chart legend. +type chartLegendOptions struct { + None bool `json:"none"` + DeleteSeries []int `json:"delete_series"` + Font Font `json:"font"` + Layout layoutOptions `json:"layout"` + Position string `json:"position"` + ShowLegendEntry bool `json:"show_legend_entry"` + ShowLegendKey bool `json:"show_legend_key"` } -// formatChartSeries directly maps the format settings of the chart series. -type formatChartSeries struct { +// chartSeriesOptions directly maps the format settings of the chart series. +type chartSeriesOptions struct { Name string `json:"name"` Categories string `json:"categories"` Values string `json:"values"` @@ -640,16 +641,16 @@ type formatChartSeries struct { } `json:"marker"` } -// formatChartTitle directly maps the format settings of the chart title. -type formatChartTitle struct { - None bool `json:"none"` - Name string `json:"name"` - Overlay bool `json:"overlay"` - Layout formatLayout `json:"layout"` +// chartTitleOptions directly maps the format settings of the chart title. +type chartTitleOptions struct { + None bool `json:"none"` + Name string `json:"name"` + Overlay bool `json:"overlay"` + Layout layoutOptions `json:"layout"` } -// formatLayout directly maps the format settings of the element layout. -type formatLayout struct { +// layoutOptions directly maps the format settings of the element layout. +type layoutOptions struct { X float64 `json:"x"` Y float64 `json:"y"` Width float64 `json:"width"` diff --git a/xmlComments.go b/xmlComments.go index b4602fc928..731f416aaa 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -72,8 +72,8 @@ type xlsxPhoneticRun struct { T string `xml:"t"` } -// formatComment directly maps the format settings of the comment. -type formatComment struct { +// commentOptions directly maps the format settings of the comment. +type commentOptions struct { Author string `json:"author"` Text string `json:"text"` } diff --git a/xmlDrawing.go b/xmlDrawing.go index fc8dee5890..6a2f79ddf5 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -493,8 +493,8 @@ type xdrTxBody struct { P []*aP `xml:"a:p"` } -// formatPicture directly maps the format settings of the picture. -type formatPicture struct { +// pictureOptions directly maps the format settings of the picture. +type pictureOptions struct { FPrintsWithSheet bool `json:"print_obj"` FLocksWithSheet bool `json:"locked"` NoChangeAspect bool `json:"lock_aspect_ratio"` @@ -508,33 +508,33 @@ type formatPicture struct { Positioning string `json:"positioning"` } -// formatShape directly maps the format settings of the shape. -type formatShape struct { - Macro string `json:"macro"` - Type string `json:"type"` - Width int `json:"width"` - Height int `json:"height"` - Format formatPicture `json:"format"` - Color formatShapeColor `json:"color"` - Line formatLine `json:"line"` - Paragraph []formatShapeParagraph `json:"paragraph"` +// shapeOptions directly maps the format settings of the shape. +type shapeOptions struct { + Macro string `json:"macro"` + Type string `json:"type"` + Width int `json:"width"` + Height int `json:"height"` + Format pictureOptions `json:"format"` + Color shapeColorOptions `json:"color"` + Line lineOptions `json:"line"` + Paragraph []shapeParagraphOptions `json:"paragraph"` } -// formatShapeParagraph directly maps the format settings of the paragraph in +// shapeParagraphOptions directly maps the format settings of the paragraph in // the shape. -type formatShapeParagraph struct { +type shapeParagraphOptions struct { Font Font `json:"font"` Text string `json:"text"` } -// formatShapeColor directly maps the color settings of the shape. -type formatShapeColor struct { +// shapeColorOptions directly maps the color settings of the shape. +type shapeColorOptions struct { Line string `json:"line"` Fill string `json:"fill"` Effect string `json:"effect"` } -// formatLine directly maps the line settings of the shape. -type formatLine struct { +// lineOptions directly maps the line settings of the shape. +type lineOptions struct { Width float64 `json:"width"` } diff --git a/xmlTable.go b/xmlTable.go index 5a56a8330d..758e0ea27c 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -196,8 +196,8 @@ type xlsxTableStyleInfo struct { ShowColumnStripes bool `xml:"showColumnStripes,attr"` } -// formatTable directly maps the format settings of the table. -type formatTable struct { +// tableOptions directly maps the format settings of the table. +type tableOptions struct { TableName string `json:"table_name"` TableStyle string `json:"table_style"` ShowFirstColumn bool `json:"show_first_column"` @@ -206,8 +206,8 @@ type formatTable struct { ShowColumnStripes bool `json:"show_column_stripes"` } -// formatAutoFilter directly maps the auto filter settings. -type formatAutoFilter struct { +// autoFilterOptions directly maps the auto filter settings. +type autoFilterOptions struct { Column string `json:"column"` Expression string `json:"expression"` FilterList []struct { diff --git a/xmlWorkbook.go b/xmlWorkbook.go index a0fce15f2e..dcfa6cad15 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -308,8 +308,15 @@ type xlsxCustomWorkbookView struct { // DefinedName directly maps the name for a cell or cell range on a // worksheet. type DefinedName struct { - Name string - Comment string - RefersTo string - Scope string + Name string `json:"name,omitempty"` + Comment string `json:"comment,omitempty"` + RefersTo string `json:"refers_to,omitempty"` + Scope string `json:"scope,omitempty"` +} + +// WorkbookPropsOptions directly maps the settings of workbook proprieties. +type WorkbookPropsOptions struct { + Date1904 *bool `json:"date_1994,omitempty"` + FilterPrivacy *bool `json:"filter_privacy,omitempty"` + CodeName *string `json:"code_name,omitempty"` } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index af7c4f3be3..28e785f228 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -454,7 +454,11 @@ type DataValidation struct { // b (Boolean) | Cell containing a boolean. // d (Date) | Cell contains a date in the ISO 8601 format. // e (Error) | Cell containing an error. -// inlineStr (Inline String) | Cell containing an (inline) rich string, i.e., one not in the shared string table. If this cell type is used, then the cell value is in the is element rather than the v element in the cell (c element). +// inlineStr (Inline String) | Cell containing an (inline) rich string, i.e., +// | one not in the shared string table. If this +// | cell type is used, then the cell value is in +// | the is element rather than the v element in +// | the cell (c element). // n (Number) | Cell containing a number. // s (Shared String) | Cell containing a shared string. // str (String) | Cell containing a formula string. @@ -777,39 +781,39 @@ type xlsxX14Sparkline struct { Sqref string `xml:"xm:sqref"` } -// SparklineOption directly maps the settings of the sparkline. -type SparklineOption struct { - Location []string - Range []string - Max int - CustMax int - Min int - CustMin int - Type string - Weight float64 - DateAxis bool - Markers bool - High bool - Low bool - First bool - Last bool - Negative bool - Axis bool - Hidden bool - Reverse bool - Style int - SeriesColor string - NegativeColor string - MarkersColor string - FirstColor string - LastColor string - HightColor string - LowColor string - EmptyCells string -} - -// formatPanes directly maps the settings of the panes. -type formatPanes struct { +// SparklineOptions directly maps the settings of the sparkline. +type SparklineOptions struct { + Location []string `json:"location"` + Range []string `json:"range"` + Max int `json:"max"` + CustMax int `json:"cust_max"` + Min int `json:"min"` + CustMin int `json:"cust_min"` + Type string `json:"hype"` + Weight float64 `json:"weight"` + DateAxis bool `json:"date_axis"` + Markers bool `json:"markers"` + High bool `json:"high"` + Low bool `json:"low"` + First bool `json:"first"` + Last bool `json:"last"` + Negative bool `json:"negative"` + Axis bool `json:"axis"` + Hidden bool `json:"hidden"` + Reverse bool `json:"reverse"` + Style int `json:"style"` + SeriesColor string `json:"series_color"` + NegativeColor string `json:"negative_color"` + MarkersColor string `json:"markers_color"` + FirstColor string `json:"first_color"` + LastColor string `json:"last_color"` + HightColor string `json:"hight_color"` + LowColor string `json:"low_color"` + EmptyCells string `json:"empty_cells"` +} + +// panesOptions directly maps the settings of the panes. +type panesOptions struct { Freeze bool `json:"freeze"` Split bool `json:"split"` XSplit int `json:"x_split"` @@ -823,8 +827,8 @@ type formatPanes struct { } `json:"panes"` } -// formatConditional directly maps the conditional format settings of the cells. -type formatConditional struct { +// conditionalOptions directly maps the conditional format settings of the cells. +type conditionalOptions struct { Type string `json:"type"` AboveAverage bool `json:"above_average,omitempty"` Percent bool `json:"percent,omitempty"` @@ -848,47 +852,163 @@ type formatConditional struct { BarColor string `json:"bar_color,omitempty"` } -// FormatSheetProtection directly maps the settings of worksheet protection. -type FormatSheetProtection struct { - AlgorithmName string - AutoFilter bool - DeleteColumns bool - DeleteRows bool - EditObjects bool - EditScenarios bool - FormatCells bool - FormatColumns bool - FormatRows bool - InsertColumns bool - InsertHyperlinks bool - InsertRows bool - Password string - PivotTables bool - SelectLockedCells bool - SelectUnlockedCells bool - Sort bool -} - -// FormatHeaderFooter directly maps the settings of header and footer. -type FormatHeaderFooter struct { - AlignWithMargins bool - DifferentFirst bool - DifferentOddEven bool - ScaleWithDoc bool - OddHeader string - OddFooter string - EvenHeader string - EvenFooter string - FirstHeader string - FirstFooter string -} - -// FormatPageMargins directly maps the settings of page margins -type FormatPageMargins struct { - Bottom string - Footer string - Header string - Left string - Right string - Top string +// SheetProtectionOptions directly maps the settings of worksheet protection. +type SheetProtectionOptions struct { + AlgorithmName string `json:"algorithm_name,omitempty"` + AutoFilter bool `json:"auto_filter,omitempty"` + DeleteColumns bool `json:"delete_columns,omitempty"` + DeleteRows bool `json:"delete_rows,omitempty"` + EditObjects bool `json:"edit_objects,omitempty"` + EditScenarios bool `json:"edit_scenarios,omitempty"` + FormatCells bool `json:"format_cells,omitempty"` + FormatColumns bool `json:"format_columns,omitempty"` + FormatRows bool `json:"format_rows,omitempty"` + InsertColumns bool `json:"insert_columns,omitempty"` + InsertHyperlinks bool `json:"insert_hyperlinks,omitempty"` + InsertRows bool `json:"insert_rows,omitempty"` + Password string `json:"password,omitempty"` + PivotTables bool `json:"pivot_tables,omitempty"` + SelectLockedCells bool `json:"select_locked_cells,omitempty"` + SelectUnlockedCells bool `json:"select_unlocked_cells,omitempty"` + Sort bool `json:"sort,omitempty"` +} + +// HeaderFooterOptions directly maps the settings of header and footer. +type HeaderFooterOptions struct { + AlignWithMargins bool `json:"align_with_margins,omitempty"` + DifferentFirst bool `json:"different_first,omitempty"` + DifferentOddEven bool `json:"different_odd_even,omitempty"` + ScaleWithDoc bool `json:"scale_with_doc,omitempty"` + OddHeader string `json:"odd_header,omitempty"` + OddFooter string `json:"odd_footer,omitempty"` + EvenHeader string `json:"even_header,omitempty"` + EvenFooter string `json:"even_footer,omitempty"` + FirstHeader string `json:"first_header,omitempty"` + FirstFooter string `json:"first_footer,omitempty"` +} + +// PageLayoutMarginsOptions directly maps the settings of page layout margins. +type PageLayoutMarginsOptions struct { + Bottom *float64 `json:"bottom,omitempty"` + Footer *float64 `json:"footer,omitempty"` + Header *float64 `json:"header,omitempty"` + Left *float64 `json:"left,omitempty"` + Right *float64 `json:"right,omitempty"` + Top *float64 `json:"top,omitempty"` + Horizontally *bool `json:"horizontally,omitempty"` + Vertically *bool `json:"vertically,omitempty"` +} + +// PageLayoutOptions directly maps the settings of page layout. +type PageLayoutOptions struct { + // Size defines the paper size of the worksheet. + Size *int `json:"size,omitempty"` + // Orientation defines the orientation of page layout for a worksheet. + Orientation *string `json:"orientation,omitempty"` + // FirstPageNumber specified the first printed page number. If no value is + // specified, then 'automatic' is assumed. + FirstPageNumber *uint `json:"first_page_number,omitempty"` + // AdjustTo defines the print scaling. This attribute is restricted to + // value ranging from 10 (10%) to 400 (400%). This setting is overridden + // when fitToWidth and/or fitToHeight are in use. + AdjustTo *uint `json:"adjust_to,omitempty"` + // FitToHeight specified the number of vertical pages to fit on. + FitToHeight *int `json:"fit_to_height,omitempty"` + // FitToWidth specified the number of horizontal pages to fit on. + FitToWidth *int `json:"fit_to_width,omitempty"` + // BlackAndWhite specified print black and white. + BlackAndWhite *bool `json:"black_and_white,omitempty"` +} + +// ViewOptions directly maps the settings of sheet view. +type ViewOptions struct { + // DefaultGridColor indicating that the consuming application should use + // the default grid lines color(system dependent). Overrides any color + // specified in colorId. + DefaultGridColor *bool `json:"default_grid_color,omitempty"` + // RightToLeft indicating whether the sheet is in 'right to left' display + // mode. When in this mode, Column A is on the far right, Column B; is one + // column left of Column A, and so on. Also, information in cells is + // displayed in the Right to Left format. + RightToLeft *bool `json:"right_to_left,omitempty"` + // ShowFormulas indicating whether this sheet should display formulas. + ShowFormulas *bool `json:"show_formulas,omitempty"` + // ShowGridLines indicating whether this sheet should display grid lines. + ShowGridLines *bool `json:"show_grid_lines,omitempty"` + // ShowRowColHeaders indicating whether the sheet should display row and + // column headings. + ShowRowColHeaders *bool `json:"show_row_col_headers,omitempty"` + // ShowRuler indicating this sheet should display ruler. + ShowRuler *bool `json:"show_ruler,omitempty"` + // ShowZeros indicating whether to "show a zero in cells that have zero + // value". When using a formula to reference another cell which is empty, + // the referenced value becomes 0 when the flag is true. (Default setting + // is true.) + ShowZeros *bool `json:"show_zeros,omitempty"` + // TopLeftCell specifies a location of the top left visible cell Location + // of the top left visible cell in the bottom right pane (when in + // Left-to-Right mode). + TopLeftCell *string `json:"top_left_cell,omitempty"` + // View indicating how sheet is displayed, by default it uses empty string + // available options: normal, pageLayout, pageBreakPreview + View *string `json:"low_color,omitempty"` + // ZoomScale specifies a window zoom magnification for current view + // representing percent values. This attribute is restricted to values + // ranging from 10 to 400. Horizontal & Vertical scale together. + ZoomScale *float64 `json:"zoom_scale,omitempty"` +} + +// SheetPropsOptions directly maps the settings of sheet view. +type SheetPropsOptions struct { + // Specifies a stable name of the sheet, which should not change over time, + // and does not change from user input. This name should be used by code + // to reference a particular sheet. + CodeName *string `json:"code_name,omitempty"` + // EnableFormatConditionsCalculation indicating whether the conditional + // formatting calculations shall be evaluated. If set to false, then the + // min/max values of color scales or data bars or threshold values in Top N + // rules shall not be updated. Essentially the conditional + // formatting "calc" is off. + EnableFormatConditionsCalculation *bool `json:"enable_format_conditions_calculation,omitempty"` + // Published indicating whether the worksheet is published. + Published *bool `json:"published,omitempty"` + // AutoPageBreaks indicating whether the sheet displays Automatic Page + // Breaks. + AutoPageBreaks *bool `json:"auto_page_breaks,omitempty"` + // FitToPage indicating whether the Fit to Page print option is enabled. + FitToPage *bool `json:"fit_to_page,omitempty"` + // TabColorIndexed represents the indexed color value. + TabColorIndexed *int `json:"tab_color_indexed,omitempty"` + // TabColorRGB represents the standard Alpha Red Green Blue color value. + TabColorRGB *string `json:"tab_color_rgb,omitempty"` + // TabColorTheme represents the zero-based index into the collection, + // referencing a particular value expressed in the Theme part. + TabColorTheme *int `json:"tab_color_theme,omitempty"` + // TabColorTint specifies the tint value applied to the color. + TabColorTint *float64 `json:"tab_color_tint,omitempty"` + // OutlineSummaryBelow indicating whether summary rows appear below detail + // in an outline, when applying an outline. + OutlineSummaryBelow *bool `json:"outline_summary_below,omitempty"` + // BaseColWidth specifies the number of characters of the maximum digit + // width of the normal style's font. This value does not include margin + // padding or extra padding for grid lines. It is only the number of + // characters. + BaseColWidth *uint8 `json:"base_col_width,omitempty"` + // DefaultColWidth specifies the default column width measured as the + // number of characters of the maximum digit width of the normal style's + // font. + DefaultColWidth *float64 `json:"default_col_width,omitempty"` + // DefaultRowHeight specifies the default row height measured in point + // size. Optimization so we don't have to write the height on all rows. + // This can be written out if most rows have custom height, to achieve the + // optimization. + DefaultRowHeight *float64 `json:"default_row_height,omitempty"` + // CustomHeight specifies the custom height. + CustomHeight *bool `json:"custom_height,omitempty"` + // ZeroHeight specifies if rows are hidden. + ZeroHeight *bool `json:"zero_height,omitempty"` + // ThickTop specifies if rows have a thick top border by default. + ThickTop *bool `json:"thick_top,omitempty"` + // ThickBottom specifies if rows have a thick bottom border by default. + ThickBottom *bool `json:"thick_bottom,omitempty"` } From 57051326d06cea02774dc0ace3293906ec5f281e Mon Sep 17 00:00:00 2001 From: Joseph Watson Date: Fri, 7 Oct 2022 00:11:59 -0400 Subject: [PATCH 110/213] This closes #1365, normalize the sheet name (#1366) Signed-off-by: Joseph Watson --- sheet.go | 1 + 1 file changed, 1 insertion(+) diff --git a/sheet.go b/sheet.go index 73e7501f27..6ec9aef863 100644 --- a/sheet.go +++ b/sheet.go @@ -457,6 +457,7 @@ func (f *File) getSheetXMLPath(sheet string) (string, bool) { name string ok bool ) + sheet = trimSheetName(sheet) for sheetName, filePath := range f.sheetMap { if strings.EqualFold(sheetName, sheet) { name, ok = filePath, true From b1e776ee33ec78b7f6c2a0de8109009963dea521 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 8 Oct 2022 22:08:06 +0800 Subject: [PATCH 111/213] Support to set summary columns to appear to the right of detail in an outline - Simplify calculation engine code - Update documentation for the functions - Update dependencies module --- calc.go | 42 +++++++++++++++++++------------------- excelize.go | 9 ++++++--- go.mod | 4 ++-- go.sum | 8 ++++---- pivotTable.go | 2 +- sheet.go | 4 ++-- sheetpr.go | 54 ++++++++++++++++++++++++++++++------------------- sheetpr_test.go | 23 +++++++++++---------- sparkline.go | 2 +- stream.go | 12 +++++------ xmlWorksheet.go | 9 ++++++--- 11 files changed, 94 insertions(+), 75 deletions(-) diff --git a/calc.go b/calc.go index b19dba749c..5d55992160 100644 --- a/calc.go +++ b/calc.go @@ -1132,7 +1132,7 @@ func calcLe(rOpd, lOpd formulaArg, opdStack *Stack) error { return nil } -// calcG evaluate greater than or equal arithmetic operations. +// calcG evaluate greater than arithmetic operations. func calcG(rOpd, lOpd formulaArg, opdStack *Stack) error { if rOpd.Type == ArgNumber && lOpd.Type == ArgNumber { opdStack.Push(newBoolFormulaArg(lOpd.Number > rOpd.Number)) @@ -1287,28 +1287,28 @@ func calculate(opdStack *Stack, opt efp.Token) error { func (f *File) parseOperatorPrefixToken(optStack, opdStack *Stack, token efp.Token) (err error) { if optStack.Len() == 0 { optStack.Push(token) - } else { - tokenPriority := getPriority(token) - topOpt := optStack.Peek().(efp.Token) - topOptPriority := getPriority(topOpt) - if tokenPriority > topOptPriority { - optStack.Push(token) - } else { - for tokenPriority <= topOptPriority { - optStack.Pop() - if err = calculate(opdStack, topOpt); err != nil { - return - } - if optStack.Len() > 0 { - topOpt = optStack.Peek().(efp.Token) - topOptPriority = getPriority(topOpt) - continue - } - break - } - optStack.Push(token) + return + } + tokenPriority := getPriority(token) + topOpt := optStack.Peek().(efp.Token) + topOptPriority := getPriority(topOpt) + if tokenPriority > topOptPriority { + optStack.Push(token) + return + } + for tokenPriority <= topOptPriority { + optStack.Pop() + if err = calculate(opdStack, topOpt); err != nil { + return + } + if optStack.Len() > 0 { + topOpt = optStack.Peek().(efp.Token) + topOptPriority = getPriority(topOpt) + continue } + break } + optStack.Push(token) return } diff --git a/excelize.go b/excelize.go index fd6a463a97..ec7485bd97 100644 --- a/excelize.go +++ b/excelize.go @@ -444,9 +444,12 @@ func (f *File) UpdateLinkedValue() error { // AddVBAProject provides the method to add vbaProject.bin file which contains // functions and/or macros. The file extension should be .xlsm. For example: // -// if err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")); err != nil { -// fmt.Println(err) -// } +// codeName := "Sheet1" +// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{ +// CodeName: &codeName, +// }); err != nil { +// fmt.Println(err) +// } // if err := f.AddVBAProject("vbaProject.bin"); err != nil { // fmt.Println(err) // } diff --git a/go.mod b/go.mod index 9d49dbee0a..1ce8df3813 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,9 @@ require ( github.com/stretchr/testify v1.7.1 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-20220829220503-c86fa9a7ed90 + golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 - golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b + golang.org/x/net v0.0.0-20221004154528-8021a29435af golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index 3f9cd78d3d..b30b6c14ba 100644 --- a/go.sum +++ b/go.sum @@ -17,13 +17,13 @@ github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj0 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-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= +golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/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-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4= +golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 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= diff --git a/pivotTable.go b/pivotTable.go index 8266c8e67f..0999a97162 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -109,7 +109,7 @@ type PivotTableField struct { // f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000)) // f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]) // } -// if err := f.AddPivotTable(&excelize.PivotTableOption{ +// if err := f.AddPivotTable(&excelize.PivotTableOptions{ // DataRange: "Sheet1!$A$1:$E$31", // PivotTableRange: "Sheet1!$G$2:$M$34", // Rows: []excelize.PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, diff --git a/sheet.go b/sheet.go index 6ec9aef863..a737a9a0d5 100644 --- a/sheet.go +++ b/sheet.go @@ -1039,7 +1039,7 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { // // For example: // -// err := f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{ +// err := f.SetHeaderFooter("Sheet1", &excelize.HeaderFooterOptions{ // DifferentFirst: true, // DifferentOddEven: true, // OddHeader: "&R&P", @@ -1109,7 +1109,7 @@ func (f *File) SetHeaderFooter(sheet string, settings *HeaderFooterOptions) erro // specified, will be using the XOR algorithm as default. For example, protect // Sheet1 with protection settings: // -// err := f.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ +// err := f.ProtectSheet("Sheet1", &excelize.SheetProtectionOptions{ // AlgorithmName: "SHA-512", // Password: "password", // EditScenarios: false, diff --git a/sheetpr.go b/sheetpr.go index a246e9ef5a..3a805c479a 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -106,41 +106,55 @@ func (f *File) GetPageMargins(sheet string) (PageLayoutMarginsOptions, error) { return opts, err } -// setSheetProps set worksheet format properties by given options. -func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) { - prepareSheetPr := func(ws *xlsxWorksheet) { - if ws.SheetPr == nil { - ws.SheetPr = new(xlsxSheetPr) +// prepareSheetPr sheetPr element if which not exist. +func (ws *xlsxWorksheet) prepareSheetPr() { + if ws.SheetPr == nil { + ws.SheetPr = new(xlsxSheetPr) + } +} + +// setSheetOutlinePr set worksheet outline properties by given options. +func (ws *xlsxWorksheet) setSheetOutlineProps(opts *SheetPropsOptions) { + prepareOutlinePr := func(ws *xlsxWorksheet) { + ws.prepareSheetPr() + if ws.SheetPr.OutlinePr == nil { + ws.SheetPr.OutlinePr = new(xlsxOutlinePr) } } + if opts.OutlineSummaryBelow != nil { + prepareOutlinePr(ws) + ws.SheetPr.OutlinePr.SummaryBelow = opts.OutlineSummaryBelow + } + if opts.OutlineSummaryRight != nil { + prepareOutlinePr(ws) + ws.SheetPr.OutlinePr.SummaryRight = opts.OutlineSummaryRight + } +} + +// setSheetProps set worksheet format properties by given options. +func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) { preparePageSetUpPr := func(ws *xlsxWorksheet) { - prepareSheetPr(ws) + ws.prepareSheetPr() if ws.SheetPr.PageSetUpPr == nil { ws.SheetPr.PageSetUpPr = new(xlsxPageSetUpPr) } } - prepareOutlinePr := func(ws *xlsxWorksheet) { - prepareSheetPr(ws) - if ws.SheetPr.OutlinePr == nil { - ws.SheetPr.OutlinePr = new(xlsxOutlinePr) - } - } prepareTabColor := func(ws *xlsxWorksheet) { - prepareSheetPr(ws) + ws.prepareSheetPr() if ws.SheetPr.TabColor == nil { ws.SheetPr.TabColor = new(xlsxTabColor) } } if opts.CodeName != nil { - prepareSheetPr(ws) + ws.prepareSheetPr() ws.SheetPr.CodeName = *opts.CodeName } if opts.EnableFormatConditionsCalculation != nil { - prepareSheetPr(ws) + ws.prepareSheetPr() ws.SheetPr.EnableFormatConditionsCalculation = opts.EnableFormatConditionsCalculation } if opts.Published != nil { - prepareSheetPr(ws) + ws.prepareSheetPr() ws.SheetPr.Published = opts.Published } if opts.AutoPageBreaks != nil { @@ -151,10 +165,7 @@ func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) { preparePageSetUpPr(ws) ws.SheetPr.PageSetUpPr.FitToPage = *opts.FitToPage } - if opts.OutlineSummaryBelow != nil { - prepareOutlinePr(ws) - ws.SheetPr.OutlinePr.SummaryBelow = *opts.OutlineSummaryBelow - } + ws.setSheetOutlineProps(opts) if opts.TabColorIndexed != nil { prepareTabColor(ws) ws.SheetPr.TabColor.Indexed = *opts.TabColorIndexed @@ -237,7 +248,8 @@ func (f *File) GetSheetProps(sheet string) (SheetPropsOptions, error) { opts.FitToPage = boolPtr(ws.SheetPr.PageSetUpPr.FitToPage) } if ws.SheetPr.OutlinePr != nil { - opts.OutlineSummaryBelow = boolPtr(ws.SheetPr.OutlinePr.SummaryBelow) + opts.OutlineSummaryBelow = ws.SheetPr.OutlinePr.SummaryBelow + opts.OutlineSummaryRight = ws.SheetPr.OutlinePr.SummaryRight } if ws.SheetPr.TabColor != nil { opts.TabColorIndexed = intPtr(ws.SheetPr.TabColor.Indexed) diff --git a/sheetpr_test.go b/sheetpr_test.go index ccadbefcb2..b4ee18dba2 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -61,25 +61,26 @@ func TestSetSheetProps(t *testing.T) { assert.True(t, ok) ws.(*xlsxWorksheet).SheetPr = nil ws.(*xlsxWorksheet).SheetFormatPr = nil - baseColWidth := uint8(8) + baseColWidth, enable := uint8(8), boolPtr(true) expected := SheetPropsOptions{ CodeName: stringPtr("code"), - EnableFormatConditionsCalculation: boolPtr(true), - Published: boolPtr(true), - AutoPageBreaks: boolPtr(true), - FitToPage: boolPtr(true), + EnableFormatConditionsCalculation: enable, + Published: enable, + AutoPageBreaks: enable, + FitToPage: enable, TabColorIndexed: intPtr(1), TabColorRGB: stringPtr("#FFFF00"), TabColorTheme: intPtr(1), TabColorTint: float64Ptr(1), - OutlineSummaryBelow: boolPtr(true), + OutlineSummaryBelow: enable, + OutlineSummaryRight: enable, BaseColWidth: &baseColWidth, DefaultColWidth: float64Ptr(10), DefaultRowHeight: float64Ptr(10), - CustomHeight: boolPtr(true), - ZeroHeight: boolPtr(true), - ThickTop: boolPtr(true), - ThickBottom: boolPtr(true), + CustomHeight: enable, + ZeroHeight: enable, + ThickTop: enable, + ThickBottom: enable, } assert.NoError(t, f.SetSheetProps("Sheet1", &expected)) opts, err := f.GetSheetProps("Sheet1") @@ -87,7 +88,7 @@ func TestSetSheetProps(t *testing.T) { assert.Equal(t, expected, opts) ws.(*xlsxWorksheet).SheetPr = nil - assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{FitToPage: boolPtr(true)})) + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{FitToPage: enable})) ws.(*xlsxWorksheet).SheetPr = nil assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{TabColorRGB: stringPtr("#FFFF00")})) ws.(*xlsxWorksheet).SheetPr = nil diff --git a/sparkline.go b/sparkline.go index f2e0d7a455..0c32462644 100644 --- a/sparkline.go +++ b/sparkline.go @@ -365,7 +365,7 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // Excel 2007, but they won't be displayed. For example, add a grouped // sparkline. Changes are applied to all three: // -// err := f.AddSparkline("Sheet1", &excelize.SparklineOption{ +// err := f.AddSparkline("Sheet1", &excelize.SparklineOptions{ // Location: []string{"A1", "A2", "A3"}, // Range: []string{"Sheet2!A1:J1", "Sheet2!A2:J2", "Sheet2!A3:J3"}, // Markers: true, diff --git a/stream.go b/stream.go index 3d06790276..b99730dc4c 100644 --- a/stream.go +++ b/stream.go @@ -40,12 +40,12 @@ type StreamWriter struct { // NewStreamWriter return stream writer struct by given worksheet name for // generate new worksheet with large amounts of data. Note that after set -// rows, you must call the 'Flush' method to end the streaming writing -// process and ensure that the order of line numbers is ascending, the common -// API and stream API can't be work mixed to writing data on the worksheets, -// you can't get cell value when in-memory chunks data over 16MB. For -// example, set data for worksheet of size 102400 rows x 50 columns with -// numbers and style: +// rows, you must call the 'Flush' method to end the streaming writing process +// and ensure that the order of line numbers is ascending, the normal mode +// functions and stream mode functions can't be work mixed to writing data on +// the worksheets, you can't get cell value when in-memory chunks data over +// 16MB. For example, set data for worksheet of size 102400 rows x 50 columns +// with numbers and style: // // file := excelize.NewFile() // streamWriter, err := file.NewStreamWriter("Sheet1") diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 28e785f228..e55406c2b6 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -250,9 +250,9 @@ type xlsxSheetPr struct { // adjust the direction of grouper controls. type xlsxOutlinePr struct { ApplyStyles *bool `xml:"applyStyles,attr"` - SummaryBelow bool `xml:"summaryBelow,attr"` - SummaryRight bool `xml:"summaryRight,attr"` - ShowOutlineSymbols bool `xml:"showOutlineSymbols,attr"` + SummaryBelow *bool `xml:"summaryBelow,attr"` + SummaryRight *bool `xml:"summaryRight,attr"` + ShowOutlineSymbols *bool `xml:"showOutlineSymbols,attr"` } // xlsxPageSetUpPr expresses page setup properties of the worksheet. @@ -989,6 +989,9 @@ type SheetPropsOptions struct { // OutlineSummaryBelow indicating whether summary rows appear below detail // in an outline, when applying an outline. OutlineSummaryBelow *bool `json:"outline_summary_below,omitempty"` + // OutlineSummaryRight indicating whether summary columns appear to the + // right of detail in an outline, when applying an outline. + OutlineSummaryRight *bool `json:"outline_summary_right,omitempty"` // BaseColWidth specifies the number of characters of the maximum digit // width of the normal style's font. This value does not include margin // padding or extra padding for grid lines. It is only the number of From 2f5704b114d033e81725f18459f9293a9adfee1e Mon Sep 17 00:00:00 2001 From: "charles.deng" Date: Mon, 10 Oct 2022 00:11:18 +0800 Subject: [PATCH 112/213] Stream writer support to set inline rich text cell (#1121) Co-authored-by: zhengchao.deng --- cell.go | 42 +++++++++++++++++++++++++----------------- excelize.go | 12 ++++++------ stream.go | 21 +++++++++++++++++++-- stream_test.go | 9 ++++++--- xmlSharedStrings.go | 5 +++-- xmlWorksheet.go | 17 ++++++++--------- 6 files changed, 67 insertions(+), 39 deletions(-) diff --git a/cell.go b/cell.go index 6beb3b27c0..80eb035268 100644 --- a/cell.go +++ b/cell.go @@ -885,6 +885,28 @@ func newRpr(fnt *Font) *xlsxRPr { return &rpr } +// setRichText provides a function to set rich text of a cell. +func setRichText(runs []RichTextRun) ([]xlsxR, error) { + var ( + textRuns []xlsxR + totalCellChars int + ) + for _, textRun := range runs { + totalCellChars += len(textRun.Text) + if totalCellChars > TotalCellChars { + return textRuns, ErrCellCharsLength + } + run := xlsxR{T: &xlsxT{}} + _, run.T.Val, run.T.Space = setCellStr(textRun.Text) + fnt := textRun.Font + if fnt != nil { + run.RPr = newRpr(fnt) + } + textRuns = append(textRuns, run) + } + return textRuns, nil +} + // SetCellRichText provides a function to set cell with rich text by given // worksheet. For example, set rich text on the A1 cell of the worksheet named // Sheet1: @@ -1016,24 +1038,10 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { return err } c.S = f.prepareCellStyle(ws, col, row, c.S) - si := xlsxSI{} - sst := f.sharedStringsReader() - var textRuns []xlsxR - totalCellChars := 0 - for _, textRun := range runs { - totalCellChars += len(textRun.Text) - if totalCellChars > TotalCellChars { - return ErrCellCharsLength - } - run := xlsxR{T: &xlsxT{}} - _, run.T.Val, run.T.Space = setCellStr(textRun.Text) - fnt := textRun.Font - if fnt != nil { - run.RPr = newRpr(fnt) - } - textRuns = append(textRuns, run) + si, sst := xlsxSI{}, f.sharedStringsReader() + if si.R, err = setRichText(runs); err != nil { + return err } - si.R = textRuns for idx, strItem := range sst.SI { if reflect.DeepEqual(strItem, si) { c.T, c.V = "s", strconv.Itoa(idx) diff --git a/excelize.go b/excelize.go index ec7485bd97..94d10885ac 100644 --- a/excelize.go +++ b/excelize.go @@ -444,12 +444,12 @@ func (f *File) UpdateLinkedValue() error { // AddVBAProject provides the method to add vbaProject.bin file which contains // functions and/or macros. The file extension should be .xlsm. For example: // -// codeName := "Sheet1" -// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{ -// CodeName: &codeName, -// }); err != nil { -// fmt.Println(err) -// } +// codeName := "Sheet1" +// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{ +// CodeName: &codeName, +// }); err != nil { +// fmt.Println(err) +// } // if err := f.AddVBAProject("vbaProject.bin"); err != nil { // fmt.Println(err) // } diff --git a/stream.go b/stream.go index b99730dc4c..66c0fda7be 100644 --- a/stream.go +++ b/stream.go @@ -56,7 +56,14 @@ type StreamWriter struct { // if err != nil { // fmt.Println(err) // } -// if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}, +// if err := streamWriter.SetRow("A1", +// []interface{}{ +// excelize.Cell{StyleID: styleID, Value: "Data"}, +// []excelize.RichTextRun{ +// {Text: "Rich ", Font: &excelize.Font{Color: "2354e8"}}, +// {Text: "Text", Font: &excelize.Font{Color: "e83723"}}, +// }, +// }, // excelize.RowOpts{Height: 45, Hidden: false}); err != nil { // fmt.Println(err) // } @@ -433,7 +440,8 @@ func setCellFormula(c *xlsxC, formula string) { } // setCellValFunc provides a function to set value of a cell. -func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) (err error) { +func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error { + var err error switch val := val.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: err = setCellIntFunc(c, val) @@ -462,6 +470,9 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) (err error) { c.T, c.V = setCellBool(val) case nil: c.T, c.V, c.XMLSpace = setCellStr("") + case []RichTextRun: + c.T, c.IS = "inlineStr", &xlsxSI{} + c.IS.R, err = setRichText(val) default: c.T, c.V, c.XMLSpace = setCellStr(fmt.Sprint(val)) } @@ -519,6 +530,12 @@ func writeCell(buf *bufferedWriter, c xlsxC) { _ = xml.EscapeText(buf, []byte(c.V)) _, _ = buf.WriteString(``) } + if c.IS != nil { + is, _ := xml.Marshal(c.IS.R) + _, _ = buf.WriteString(``) + _, _ = buf.Write(is) + _, _ = buf.WriteString(``) + } _, _ = buf.WriteString(``) } diff --git a/stream_test.go b/stream_test.go index 80875c79a6..3c2cc691d6 100644 --- a/stream_test.go +++ b/stream_test.go @@ -52,11 +52,14 @@ func TestStreamWriter(t *testing.T) { row[0] = []byte("Word") assert.NoError(t, streamWriter.SetRow("A3", row)) - // Test set cell with style. + // Test set cell with style and rich text. styleID, err := file.NewStyle(&Style{Font: &Font{Color: "#777777"}}) assert.NoError(t, err) assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}, Cell{Formula: "SUM(A10,B10)"}}, RowOpts{Height: 45, StyleID: styleID})) - assert.NoError(t, streamWriter.SetRow("A5", []interface{}{&Cell{StyleID: styleID, Value: "cell"}, &Cell{Formula: "SUM(A10,B10)"}})) + assert.NoError(t, streamWriter.SetRow("A5", []interface{}{&Cell{StyleID: styleID, Value: "cell"}, &Cell{Formula: "SUM(A10,B10)"}, []RichTextRun{ + {Text: "Rich ", Font: &Font{Color: "2354e8"}}, + {Text: "Text", Font: &Font{Color: "e83723"}}, + }})) assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()})) assert.NoError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: 20, Hidden: true, StyleID: styleID})) assert.EqualError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: MaxRowHeight + 1}), ErrMaxRowHeight.Error()) @@ -128,7 +131,7 @@ func TestStreamWriter(t *testing.T) { cells += len(row) } assert.NoError(t, rows.Close()) - assert.Equal(t, 2559558, cells) + assert.Equal(t, 2559559, cells) // Save spreadsheet with password. assert.NoError(t, file.SaveAs(filepath.Join("test", "EncryptionTestStreamWriter.xlsx"), Options{Password: "password"})) assert.NoError(t, file.Close()) diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 683105e3ca..3249ecacf7 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -46,8 +46,9 @@ type xlsxSI struct { // properties are defined in the rPr element, and the text displayed to the // user is defined in the Text (t) element. type xlsxR struct { - RPr *xlsxRPr `xml:"rPr"` - T *xlsxT `xml:"t"` + XMLName xml.Name `xml:"r"` + RPr *xlsxRPr `xml:"rPr"` + T *xlsxT `xml:"t"` } // xlsxT directly maps the t element in the run properties. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index e55406c2b6..24f5e4e5f9 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -466,15 +466,14 @@ type xlsxC struct { XMLName xml.Name `xml:"c"` XMLSpace xml.Attr `xml:"space,attr,omitempty"` R string `xml:"r,attr,omitempty"` // Cell ID, e.g. A1 - S int `xml:"s,attr,omitempty"` // Style reference. - // Str string `xml:"str,attr,omitempty"` // Style reference. - T string `xml:"t,attr,omitempty"` // Type. - Cm *uint `xml:"cm,attr,omitempty"` // - Vm *uint `xml:"vm,attr,omitempty"` // - Ph *bool `xml:"ph,attr,omitempty"` // - F *xlsxF `xml:"f,omitempty"` // Formula - V string `xml:"v,omitempty"` // Value - IS *xlsxSI `xml:"is"` + S int `xml:"s,attr,omitempty"` // Style reference + T string `xml:"t,attr,omitempty"` // Type + Cm *uint `xml:"cm,attr"` + Vm *uint `xml:"vm,attr"` + Ph *bool `xml:"ph,attr"` + F *xlsxF `xml:"f"` // Formula + V string `xml:"v,omitempty"` // Value + IS *xlsxSI `xml:"is"` } // xlsxF represents a formula for the cell. The formula expression is From c02346bafc6e098406f32ee0a183d45f3038c619 Mon Sep 17 00:00:00 2001 From: Harrison Date: Mon, 10 Oct 2022 13:05:02 -0300 Subject: [PATCH 113/213] This closes #1047, stream writer support set panes (#1123) - New exported error `ErrStreamSetPanes` has been added --- errors.go | 3 +++ sheet.go | 68 ++++++++++++++++++++++++++++---------------------- sheet_test.go | 2 +- stream.go | 35 ++++++++++++++++++-------- stream_test.go | 12 ++++++++- 5 files changed, 78 insertions(+), 42 deletions(-) diff --git a/errors.go b/errors.go index 48476bc406..fd896a6303 100644 --- a/errors.go +++ b/errors.go @@ -91,6 +91,9 @@ var ( // ErrStreamSetColWidth defined the error message on set column width in // stream writing mode. ErrStreamSetColWidth = errors.New("must call the SetColWidth function before the SetRow function") + // ErrStreamSetPanes defined the error message on set panes in stream + // writing mode. + ErrStreamSetPanes = errors.New("must call the SetPanes function before the SetRow function") // ErrColumnNumber defined the error message on receive an invalid column // number. ErrColumnNumber = fmt.Errorf(`the column number must be greater than or equal to %d and less than or equal to %d`, MinColumns, MaxColumns) diff --git a/sheet.go b/sheet.go index a737a9a0d5..71123d7675 100644 --- a/sheet.go +++ b/sheet.go @@ -683,8 +683,44 @@ func parsePanesOptions(opts string) (*panesOptions, error) { return &format, err } +// setPanes set create freeze panes and split panes by given options. +func (ws *xlsxWorksheet) setPanes(panes string) error { + opts, err := parsePanesOptions(panes) + if err != nil { + return err + } + p := &xlsxPane{ + ActivePane: opts.ActivePane, + TopLeftCell: opts.TopLeftCell, + XSplit: float64(opts.XSplit), + YSplit: float64(opts.YSplit), + } + if opts.Freeze { + p.State = "frozen" + } + if ws.SheetViews == nil { + ws.SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{{}}} + } + ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = p + if !(opts.Freeze) && !(opts.Split) { + if len(ws.SheetViews.SheetView) > 0 { + ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = nil + } + } + var s []*xlsxSelection + for _, p := range opts.Panes { + s = append(s, &xlsxSelection{ + ActiveCell: p.ActiveCell, + Pane: p.Pane, + SQRef: p.SQRef, + }) + } + ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Selection = s + return err +} + // SetPanes provides a function to create and remove freeze panes and split panes -// by given worksheet name and panes format set. +// by given worksheet name and panes options. // // activePane defines the pane that is active. The possible values for this // attribute are defined in the following table: @@ -768,39 +804,11 @@ func parsePanesOptions(opts string) (*panesOptions, error) { // // f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) func (f *File) SetPanes(sheet, panes string) error { - fs, _ := parsePanesOptions(panes) ws, err := f.workSheetReader(sheet) if err != nil { return err } - p := &xlsxPane{ - ActivePane: fs.ActivePane, - TopLeftCell: fs.TopLeftCell, - XSplit: float64(fs.XSplit), - YSplit: float64(fs.YSplit), - } - if fs.Freeze { - p.State = "frozen" - } - if ws.SheetViews == nil { - ws.SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{{}}} - } - ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = p - if !(fs.Freeze) && !(fs.Split) { - if len(ws.SheetViews.SheetView) > 0 { - ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = nil - } - } - var s []*xlsxSelection - for _, p := range fs.Panes { - s = append(s, &xlsxSelection{ - ActiveCell: p.ActiveCell, - Pane: p.Pane, - SQRef: p.SQRef, - }) - } - ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Selection = s - return err + return ws.setPanes(panes) } // GetSheetVisible provides a function to get worksheet visible by given worksheet diff --git a/sheet_test.go b/sheet_test.go index 74ca02c1b3..6e87de9cb0 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -34,7 +34,7 @@ func TestSetPane(t *testing.T) { assert.NoError(t, f.SetPanes("Panes 3", `{"freeze":false,"split":true,"x_split":3270,"y_split":1800,"top_left_cell":"N57","active_pane":"bottomLeft","panes":[{"sqref":"I36","active_cell":"I36"},{"sqref":"G33","active_cell":"G33","pane":"topRight"},{"sqref":"J60","active_cell":"J60","pane":"bottomLeft"},{"sqref":"O60","active_cell":"O60","pane":"bottomRight"}]}`)) f.NewSheet("Panes 4") assert.NoError(t, f.SetPanes("Panes 4", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`)) - assert.NoError(t, f.SetPanes("Panes 4", "")) + assert.EqualError(t, f.SetPanes("Panes 4", ""), "unexpected end of JSON input") assert.EqualError(t, f.SetPanes("SheetN", ""), "sheet SheetN does not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) // Test add pane on empty sheet views worksheet diff --git a/stream.go b/stream.go index 66c0fda7be..9f477628fb 100644 --- a/stream.go +++ b/stream.go @@ -119,7 +119,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { f.streams[sheetXMLPath] = sw _, _ = sw.rawData.WriteString(xml.Header + ` 0 { - _, _ = sw.rawData.WriteString("" + sw.cols + "") - } - _, _ = sw.rawData.WriteString(``) - sw.sheetWritten = true - } + sw.writeSheetData() options := parseRowOpts(opts...) attrs, err := options.marshalAttrs() if err != nil { @@ -415,6 +409,16 @@ func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { return nil } +// SetPanes provides a function to create and remove freeze panes and split +// panes by given worksheet name and panes options for the StreamWriter. Note +// that you must call the 'SetPanes' function before the 'SetRow' function. +func (sw *StreamWriter) SetPanes(panes string) error { + if sw.sheetWritten { + return ErrStreamSetPanes + } + return sw.worksheet.setPanes(panes) +} + // MergeCell provides a function to merge cells by a given range reference for // the StreamWriter. Don't create a merged cell that overlaps with another // existing merged cell. @@ -507,6 +511,7 @@ func setCellIntFunc(c *xlsxC, val interface{}) (err error) { return } +// writeCell constructs a cell XML and writes it to the buffer. func writeCell(buf *bufferedWriter, c xlsxC) { _, _ = buf.WriteString(``) } -// Flush ending the streaming writing process. -func (sw *StreamWriter) Flush() error { +// writeSheetData prepares the element preceding sheetData and writes the +// sheetData XML start element to the buffer. +func (sw *StreamWriter) writeSheetData() { if !sw.sheetWritten { + bulkAppendFields(&sw.rawData, sw.worksheet, 4, 5) + if len(sw.cols) > 0 { + _, _ = sw.rawData.WriteString("" + sw.cols + "") + } _, _ = sw.rawData.WriteString(``) sw.sheetWritten = true } +} + +// Flush ending the streaming writing process. +func (sw *StreamWriter) Flush() error { + sw.writeSheetData() _, _ = sw.rawData.WriteString(``) bulkAppendFields(&sw.rawData, sw.worksheet, 8, 15) mergeCells := strings.Builder{} diff --git a/stream_test.go b/stream_test.go index 3c2cc691d6..91aa58099e 100644 --- a/stream_test.go +++ b/stream_test.go @@ -146,7 +146,17 @@ func TestStreamSetColWidth(t *testing.T) { assert.ErrorIs(t, streamWriter.SetColWidth(MaxColumns+1, 3, 20), ErrColumnNumber) assert.EqualError(t, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1), ErrColumnWidth.Error()) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) - assert.EqualError(t, streamWriter.SetColWidth(2, 3, 20), ErrStreamSetColWidth.Error()) + assert.ErrorIs(t, streamWriter.SetColWidth(2, 3, 20), ErrStreamSetColWidth) +} + +func TestStreamSetPanes(t *testing.T) { + file, paneOpts := NewFile(), `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}` + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + assert.NoError(t, streamWriter.SetPanes(paneOpts)) + assert.EqualError(t, streamWriter.SetPanes(""), "unexpected end of JSON input") + assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) + assert.ErrorIs(t, streamWriter.SetPanes(paneOpts), ErrStreamSetPanes) } func TestStreamTable(t *testing.T) { From 0e657c887bf505d62ce3bf685c518cd0ed7bc558 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 12 Oct 2022 00:06:09 +0800 Subject: [PATCH 114/213] This closes #1368, fixes number parsing issue, adds support for create a 3D line chart --- cell_test.go | 27 ++++++++++++--------------- chart.go | 10 ++++++++++ chart_test.go | 3 ++- drawing.go | 26 +++++++++++++++++++++++++- lib.go | 3 +++ styles.go | 21 +++++++++++++++++++++ xmlChart.go | 1 + 7 files changed, 74 insertions(+), 17 deletions(-) diff --git a/cell_test.go b/cell_test.go index 0205705107..759805835b 100644 --- a/cell_test.go +++ b/cell_test.go @@ -683,51 +683,48 @@ func TestSetCellRichText(t *testing.T) { func TestFormattedValue2(t *testing.T) { f := NewFile() - v := f.formattedValue(0, "43528", false) - assert.Equal(t, "43528", v) + assert.Equal(t, "43528", f.formattedValue(0, "43528", false)) - v = f.formattedValue(15, "43528", false) - assert.Equal(t, "43528", v) + assert.Equal(t, "43528", f.formattedValue(15, "43528", false)) - v = f.formattedValue(1, "43528", false) - assert.Equal(t, "43528", v) + assert.Equal(t, "43528", f.formattedValue(1, "43528", false)) customNumFmt := "[$-409]MM/DD/YYYY" _, err := f.NewStyle(&Style{ CustomNumFmt: &customNumFmt, }) assert.NoError(t, err) - v = f.formattedValue(1, "43528", false) - assert.Equal(t, "03/04/2019", v) + assert.Equal(t, "03/04/2019", f.formattedValue(1, "43528", false)) // formatted value with no built-in number format ID numFmtID := 5 f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: &numFmtID, }) - v = f.formattedValue(2, "43528", false) - assert.Equal(t, "43528", v) + assert.Equal(t, "43528", f.formattedValue(2, "43528", false)) // formatted value with invalid number format ID f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: nil, }) - _ = f.formattedValue(3, "43528", false) + assert.Equal(t, "43528", f.formattedValue(3, "43528", false)) // formatted value with empty number format f.Styles.NumFmts = nil f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: &numFmtID, }) - v = f.formattedValue(1, "43528", false) - assert.Equal(t, "43528", v) + assert.Equal(t, "43528", f.formattedValue(1, "43528", false)) // formatted decimal value with build-in number format ID styleID, err := f.NewStyle(&Style{ NumFmt: 1, }) assert.NoError(t, err) - v = f.formattedValue(styleID, "310.56", false) - assert.Equal(t, "311", v) + assert.Equal(t, "311", f.formattedValue(styleID, "310.56", false)) + + for _, fn := range builtInNumFmtFunc { + assert.Equal(t, "0_0", fn("0_0", "", false)) + } } func TestSharedStringsError(t *testing.T) { diff --git a/chart.go b/chart.go index 24ad2076eb..0caa505480 100644 --- a/chart.go +++ b/chart.go @@ -63,6 +63,7 @@ const ( Col3DCylinderPercentStacked = "col3DCylinderPercentStacked" Doughnut = "doughnut" Line = "line" + Line3D = "line3D" Pie = "pie" Pie3D = "pie3D" PieOfPieChart = "pieOfPie" @@ -122,6 +123,7 @@ var ( Col3DCylinderPercentStacked: 15, Doughnut: 0, Line: 0, + Line3D: 20, Pie: 0, Pie3D: 30, PieOfPieChart: 0, @@ -176,6 +178,7 @@ var ( Col3DCylinderPercentStacked: 20, Doughnut: 0, Line: 0, + Line3D: 15, Pie: 0, Pie3D: 0, PieOfPieChart: 0, @@ -194,6 +197,7 @@ var ( ColPercentStacked: 100, } chartView3DPerspective = map[string]int{ + Line3D: 30, Contour: 0, WireframeContour: 0, } @@ -240,6 +244,7 @@ var ( Col3DCylinderPercentStacked: 1, Doughnut: 0, Line: 0, + Line3D: 0, Pie: 0, Pie3D: 0, PieOfPieChart: 0, @@ -302,6 +307,7 @@ var ( Col3DCylinderPercentStacked: "0%", Doughnut: "General", Line: "General", + Line3D: "General", Pie: "General", Pie3D: "General", PieOfPieChart: "General", @@ -358,6 +364,7 @@ var ( Col3DCylinderPercentStacked: "between", Doughnut: "between", Line: "between", + Line3D: "between", Pie: "between", Pie3D: "between", PieOfPieChart: "between", @@ -413,6 +420,7 @@ var ( Col3DCylinderStacked: "stacked", Col3DCylinderPercentStacked: "percentStacked", Line: "standard", + Line3D: "standard", } plotAreaChartBarDir = map[string]string{ Bar: "bar", @@ -450,6 +458,7 @@ var ( Col3DCylinderStacked: "col", Col3DCylinderPercentStacked: "col", Line: "standard", + Line3D: "standard", } orientation = map[bool]string{ true: "maxMin", @@ -624,6 +633,7 @@ func parseChartOptions(opts string) (*chartOptions, error) { // col3DCylinderPercentStacked | 3D cylinder percent stacked column chart // doughnut | doughnut chart // line | line chart +// line3D | 3D line chart // pie | pie chart // pie3D | 3D pie chart // pieOfPie | pie of pie chart diff --git a/chart_test.go b/chart_test.go index bd633761ab..3c80b390ea 100644 --- a/chart_test.go +++ b/chart_test.go @@ -122,6 +122,7 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.AddChart("Sheet1", "X16", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"3D Clustered Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet1", "P30", `{"type":"col3DStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet1", "X30", `{"type":"col3DPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) + assert.NoError(t, f.AddChart("Sheet1", "X45", `{"type":"radar","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top_right","show_legend_key":false},"title":{"name":"Radar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"span"}`)) assert.NoError(t, f.AddChart("Sheet1", "AF1", `{"type":"col3DConeStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cone Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet1", "AF16", `{"type":"col3DConeClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cone Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet1", "AF30", `{"type":"col3DConePercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cone Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) @@ -135,7 +136,7 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.AddChart("Sheet1", "AV30", `{"type":"col3DCylinderPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet1", "AV45", `{"type":"col3DCylinder","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet1", "P45", `{"type":"col3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P1", `{"type":"radar","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top_right","show_legend_key":false},"title":{"name":"Radar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"span"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P1", `{"type":"line3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30","marker":{"symbol":"none","size":10}, "line":{"color":"#000000"}},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top","show_legend_key":false},"title":{"name":"3D Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true,"minor_grid_lines":true,"tick_label_skip":1},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1}}`)) assert.NoError(t, f.AddChart("Sheet2", "X1", `{"type":"scatter","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"Scatter Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"right","show_legend_key":false},"title":{"name":"Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero","hole_size":30}`)) assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30","marker":{"symbol":"none","size":10}, "line":{"color":"#000000"}},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top","show_legend_key":false},"title":{"name":"Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true,"minor_grid_lines":true,"tick_label_skip":1},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1}}`)) diff --git a/drawing.go b/drawing.go index 3ef58212b3..2da2573bc3 100644 --- a/drawing.go +++ b/drawing.go @@ -224,8 +224,9 @@ func (f *File) addChart(opts *chartOptions, comboCharts []*chartOptions) { Col3DCylinderPercentStacked: f.drawBaseChart, Doughnut: f.drawDoughnutChart, Line: f.drawLineChart, - Pie3D: f.drawPie3DChart, + Line3D: f.drawLine3DChart, Pie: f.drawPieChart, + Pie3D: f.drawPie3DChart, PieOfPieChart: f.drawPieOfPieChart, BarOfPieChart: f.drawBarOfPieChart, Radar: f.drawRadarChart, @@ -553,6 +554,29 @@ func (f *File) drawLineChart(opts *chartOptions) *cPlotArea { } } +// drawLine3DChart provides a function to draw the c:plotArea element for line +// chart by given format sets. +func (f *File) drawLine3DChart(opts *chartOptions) *cPlotArea { + return &cPlotArea{ + Line3DChart: &cCharts{ + Grouping: &attrValString{ + Val: stringPtr(plotAreaChartGrouping[opts.Type]), + }, + VaryColors: &attrValBool{ + Val: boolPtr(false), + }, + Ser: f.drawChartSeries(opts), + DLbls: f.drawChartDLbls(opts), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + }, + }, + CatAx: f.drawPlotAreaCatAx(opts), + ValAx: f.drawPlotAreaValAx(opts), + } +} + // drawPieChart provides a function to draw the c:plotArea element for pie // chart by given format sets. func (f *File) drawPieChart(opts *chartOptions) *cPlotArea { diff --git a/lib.go b/lib.go index 945c6f0f9c..7f388e0154 100644 --- a/lib.go +++ b/lib.go @@ -688,6 +688,9 @@ func (f *File) addSheetNameSpace(sheet string, ns xml.Attr) { // isNumeric determines whether an expression is a valid numeric type and get // the precision for the numeric. func isNumeric(s string) (bool, int, float64) { + if strings.Contains(s, "_") { + return false, 0, 0 + } var decimal big.Float _, ok := decimal.SetString(s) if !ok { diff --git a/styles.go b/styles.go index 6d90a9ec3a..5299fbd58b 100644 --- a/styles.go +++ b/styles.go @@ -873,6 +873,9 @@ var operatorType = map[string]string{ // format as string type by given built-in number formats code and cell // string. func formatToInt(v, format string, date1904 bool) string { + if strings.Contains(v, "_") { + return v + } f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -884,6 +887,9 @@ func formatToInt(v, format string, date1904 bool) string { // format as string type by given built-in number formats code and cell // string. func formatToFloat(v, format string, date1904 bool) string { + if strings.Contains(v, "_") { + return v + } f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -894,6 +900,9 @@ func formatToFloat(v, format string, date1904 bool) string { // formatToA provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. func formatToA(v, format string, date1904 bool) string { + if strings.Contains(v, "_") { + return v + } f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -907,6 +916,9 @@ func formatToA(v, format string, date1904 bool) string { // formatToB provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. func formatToB(v, format string, date1904 bool) string { + if strings.Contains(v, "_") { + return v + } f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -920,6 +932,9 @@ func formatToB(v, format string, date1904 bool) string { // formatToC provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. func formatToC(v, format string, date1904 bool) string { + if strings.Contains(v, "_") { + return v + } f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -930,6 +945,9 @@ func formatToC(v, format string, date1904 bool) string { // formatToD provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. func formatToD(v, format string, date1904 bool) string { + if strings.Contains(v, "_") { + return v + } f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -940,6 +958,9 @@ func formatToD(v, format string, date1904 bool) string { // formatToE provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. func formatToE(v, format string, date1904 bool) string { + if strings.Contains(v, "_") { + return v + } f, err := strconv.ParseFloat(v, 64) if err != nil { return v diff --git a/xmlChart.go b/xmlChart.go index 53755f37ad..9024770b02 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -309,6 +309,7 @@ type cPlotArea struct { BubbleChart *cCharts `xml:"bubbleChart"` DoughnutChart *cCharts `xml:"doughnutChart"` LineChart *cCharts `xml:"lineChart"` + Line3DChart *cCharts `xml:"line3DChart"` PieChart *cCharts `xml:"pieChart"` Pie3DChart *cCharts `xml:"pie3DChart"` OfPieChart *cCharts `xml:"ofPieChart"` From 7363c1e3337c5f0d9c70cc8af7504b3f8c092ab4 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 13 Oct 2022 00:02:53 +0800 Subject: [PATCH 115/213] Go 1.16 and later required, migration of deprecation package `ioutil` - Improving performance for stream writer `SetRow` function, reduces memory usage over and speedup about 19% - Update dependencies module - Update GitHub workflow --- .github/workflows/go.yml | 2 +- README.md | 2 +- README_zh.md | 2 +- crypt_test.go | 4 +-- excelize.go | 5 ++- excelize_test.go | 3 +- go.mod | 17 +++++----- go.sum | 40 +++++++++++++++++------ lib.go | 5 ++- picture.go | 9 +++--- picture_test.go | 12 +++---- rows.go | 3 +- sheet.go | 3 +- stream.go | 68 +++++++++++++++++++++++++++++----------- stream_test.go | 3 +- 15 files changed, 110 insertions(+), 68 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 4026b719ba..46f92f5fe4 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,7 +5,7 @@ jobs: test: strategy: matrix: - go-version: [1.15.x, 1.16.x, 1.17.x, 1.18.x, 1.19.x] + go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x] os: [ubuntu-latest, macos-latest, windows-latest] targetplatform: [x86, x64] diff --git a/README.md b/README.md index 6ab549edf8..008efc51b5 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ ## Introduction -Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge amounts of data. This library needs Go version 1.15 or later. The full API docs can be seen using go's built-in documentation tool, or online at [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) and [docs reference](https://xuri.me/excelize/). +Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge amounts of data. This library needs Go version 1.16 or later. The full docs can be seen using go's built-in documentation tool, or online at [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) and [docs reference](https://xuri.me/excelize/). ## Basic Usage diff --git a/README_zh.md b/README_zh.md index d67b63cb03..212bf79be6 100644 --- a/README_zh.md +++ b/README_zh.md @@ -13,7 +13,7 @@ ## 简介 -Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.15 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。 +Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写函数,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.16 或更高版本,完整的使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/crypt_test.go b/crypt_test.go index 95b6f52cc4..a4a510e2a8 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -12,7 +12,7 @@ package excelize import ( - "io/ioutil" + "os" "path/filepath" "strings" "testing" @@ -32,7 +32,7 @@ func TestEncrypt(t *testing.T) { assert.Equal(t, "SECRET", cell) assert.NoError(t, f.Close()) // Test decrypt spreadsheet with unsupported encrypt mechanism - raw, err := ioutil.ReadFile(filepath.Join("test", "encryptAES.xlsx")) + raw, err := os.ReadFile(filepath.Join("test", "encryptAES.xlsx")) assert.NoError(t, err) raw[2050] = 3 _, err = Decrypt(raw, &Options{Password: "password"}) diff --git a/excelize.go b/excelize.go index 94d10885ac..987314bf81 100644 --- a/excelize.go +++ b/excelize.go @@ -18,7 +18,6 @@ import ( "encoding/xml" "fmt" "io" - "io/ioutil" "os" "path" "path/filepath" @@ -137,7 +136,7 @@ func newFile() *File { // OpenReader read data stream from io.Reader and return a populated // spreadsheet file. func OpenReader(r io.Reader, opts ...Options) (*File, error) { - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err != nil { return nil, err } @@ -488,7 +487,7 @@ func (f *File) AddVBAProject(bin string) error { Type: SourceRelationshipVBAProject, }) } - file, _ := ioutil.ReadFile(filepath.Clean(bin)) + file, _ := os.ReadFile(filepath.Clean(bin)) f.Pkg.Store("xl/vbaProject.bin", file) return err } diff --git a/excelize_test.go b/excelize_test.go index e685b669ee..12d155d6c4 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -10,7 +10,6 @@ import ( _ "image/gif" _ "image/jpeg" _ "image/png" - "io/ioutil" "math" "os" "path/filepath" @@ -1388,7 +1387,7 @@ func prepareTestBook1() (*File, error) { return nil, err } - file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.jpg")) + file, err := os.ReadFile(filepath.Join("test", "images", "excel.jpg")) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 1ce8df3813..644a6aaad5 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,17 @@ module github.com/xuri/excelize/v2 -go 1.15 +go 1.16 require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.4 - github.com/richardlehane/msoleps v1.0.3 // indirect - github.com/stretchr/testify v1.7.1 + github.com/stretchr/testify v1.8.0 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-20221005025214-4161e89ecf1b - golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 - golang.org/x/net v0.0.0-20221004154528-8021a29435af - golang.org/x/text v0.3.7 - gopkg.in/yaml.v3 v3.0.0 // indirect + golang.org/x/crypto v0.0.0-20221012134737-56aed061732a + golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 + golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 + golang.org/x/text v0.3.8 ) + +require github.com/richardlehane/msoleps v1.0.3 // indirect diff --git a/go.sum b/go.sum index b30b6c14ba..69d81ecaa8 100644 --- a/go.sum +++ b/go.sum @@ -11,31 +11,51 @@ github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTK github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= 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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= -golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/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= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg= +golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= +golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4= -golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 h1:MgJ6t2zo8v0tbmLCueaCbF1RM+TtB0rs3Lv8DGtOIpY= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lib.go b/lib.go index 7f388e0154..685571c487 100644 --- a/lib.go +++ b/lib.go @@ -18,7 +18,6 @@ import ( "encoding/xml" "fmt" "io" - "io/ioutil" "math/big" "os" "regexp" @@ -73,7 +72,7 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { // unzipToTemp unzip the zip entity to the system temporary directory and // returned the unzipped file path. func (f *File) unzipToTemp(zipFile *zip.File) (string, error) { - tmp, err := ioutil.TempFile(os.TempDir(), "excelize-") + tmp, err := os.CreateTemp(os.TempDir(), "excelize-") if err != nil { return "", err } @@ -111,7 +110,7 @@ func (f *File) readBytes(name string) []byte { if err != nil { return content } - content, _ = ioutil.ReadAll(file) + content, _ = io.ReadAll(file) f.Pkg.Store(name, content) _ = file.Close() return content diff --git a/picture.go b/picture.go index aceb3f43bd..05e4a51644 100644 --- a/picture.go +++ b/picture.go @@ -17,7 +17,6 @@ import ( "encoding/xml" "image" "io" - "io/ioutil" "os" "path" "path/filepath" @@ -115,7 +114,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { if !ok { return ErrImgExt } - file, _ := ioutil.ReadFile(filepath.Clean(picture)) + file, _ := os.ReadFile(filepath.Clean(picture)) _, name := filepath.Split(picture) return f.AddPictureFromBytes(sheet, cell, format, name, ext, file) } @@ -129,7 +128,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // import ( // "fmt" // _ "image/jpeg" -// "io/ioutil" +// "os" // // "github.com/xuri/excelize/v2" // ) @@ -137,7 +136,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // func main() { // f := excelize.NewFile() // -// file, err := ioutil.ReadFile("image.jpg") +// file, err := os.ReadFile("image.jpg") // if err != nil { // fmt.Println(err) // } @@ -486,7 +485,7 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // fmt.Println(err) // return // } -// if err := ioutil.WriteFile(file, raw, 0644); err != nil { +// if err := os.WriteFile(file, raw, 0644); err != nil { // fmt.Println(err) // } func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { diff --git a/picture_test.go b/picture_test.go index d419378ba5..e90de20a35 100644 --- a/picture_test.go +++ b/picture_test.go @@ -7,7 +7,7 @@ import ( _ "image/jpeg" _ "image/png" "io" - "io/ioutil" + "os" "path/filepath" "strings" "testing" @@ -19,7 +19,7 @@ import ( func BenchmarkAddPictureFromBytes(b *testing.B) { f := NewFile() - imgFile, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png")) + imgFile, err := os.ReadFile(filepath.Join("test", "images", "excel.png")) if err != nil { b.Error("unable to load image for benchmark") } @@ -42,7 +42,7 @@ func TestAddPicture(t *testing.T) { assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)) - file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png")) + file, err := os.ReadFile(filepath.Join("test", "images", "excel.png")) assert.NoError(t, err) // Test add picture to worksheet with autofit. @@ -114,7 +114,7 @@ func TestGetPicture(t *testing.T) { file, raw, err := f.GetPicture("Sheet1", "F21") assert.NoError(t, err) if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) || - !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0o644)) { + !assert.NoError(t, os.WriteFile(filepath.Join("test", file), raw, 0o644)) { t.FailNow() } @@ -148,7 +148,7 @@ func TestGetPicture(t *testing.T) { file, raw, err = f.GetPicture("Sheet1", "F21") assert.NoError(t, err) if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) || - !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0o644)) { + !assert.NoError(t, os.WriteFile(filepath.Join("test", file), raw, 0o644)) { t.FailNow() } @@ -180,7 +180,7 @@ func TestAddDrawingPicture(t *testing.T) { func TestAddPictureFromBytes(t *testing.T) { f := NewFile() - imgFile, err := ioutil.ReadFile("logo.png") + imgFile, err := os.ReadFile("logo.png") assert.NoError(t, err, "Unable to load logo for test") assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile)) assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "", "logo", ".png", imgFile)) diff --git a/rows.go b/rows.go index 2960aa461b..1fd6825ba9 100644 --- a/rows.go +++ b/rows.go @@ -16,7 +16,6 @@ import ( "encoding/xml" "fmt" "io" - "io/ioutil" "log" "math" "os" @@ -296,7 +295,7 @@ func (f *File) getFromStringItem(index int) string { defer tempFile.Close() } f.sharedStringItem = [][]uint{} - f.sharedStringTemp, _ = ioutil.TempFile(os.TempDir(), "excelize-") + f.sharedStringTemp, _ = os.CreateTemp(os.TempDir(), "excelize-") f.tempFiles.Store(defaultTempFileSST, f.sharedStringTemp.Name()) var ( inElement string diff --git a/sheet.go b/sheet.go index 71123d7675..ecd39f069a 100644 --- a/sheet.go +++ b/sheet.go @@ -17,7 +17,6 @@ import ( "encoding/xml" "fmt" "io" - "io/ioutil" "log" "os" "path" @@ -479,7 +478,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { if !ok { return ErrImgExt } - file, _ := ioutil.ReadFile(filepath.Clean(picture)) + file, _ := os.ReadFile(filepath.Clean(picture)) name := f.addMedia(file, ext) sheetXMLPath, _ := f.getSheetXMLPath(sheet) sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" diff --git a/stream.go b/stream.go index 9f477628fb..62470b59bb 100644 --- a/stream.go +++ b/stream.go @@ -16,7 +16,6 @@ import ( "encoding/xml" "fmt" "io" - "io/ioutil" "os" "reflect" "strconv" @@ -30,7 +29,7 @@ type StreamWriter struct { Sheet string SheetID int sheetWritten bool - cols string + cols strings.Builder worksheet *xlsxWorksheet rawData bufferedWriter mergeCellsCount int @@ -310,24 +309,32 @@ type RowOpts struct { } // marshalAttrs prepare attributes of the row. -func (r *RowOpts) marshalAttrs() (attrs string, err error) { +func (r *RowOpts) marshalAttrs() (strings.Builder, error) { + var ( + err error + attrs strings.Builder + ) if r == nil { - return + return attrs, err } if r.Height > MaxRowHeight { err = ErrMaxRowHeight - return + return attrs, err } if r.StyleID > 0 { - attrs += fmt.Sprintf(` s="%d" customFormat="true"`, r.StyleID) + attrs.WriteString(` s="`) + attrs.WriteString(strconv.Itoa(r.StyleID)) + attrs.WriteString(`" customFormat="1"`) } if r.Height > 0 { - attrs += fmt.Sprintf(` ht="%v" customHeight="true"`, r.Height) + attrs.WriteString(` ht="`) + attrs.WriteString(strconv.FormatFloat(r.Height, 'f', -1, 64)) + attrs.WriteString(`" customHeight="1"`) } if r.Hidden { - attrs += ` hidden="true"` + attrs.WriteString(` hidden="1"`) } - return + return attrs, err } // parseRowOpts provides a function to parse the optional settings for @@ -357,7 +364,11 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt if err != nil { return err } - _, _ = fmt.Fprintf(&sw.rawData, ``, row, attrs) + sw.rawData.WriteString(``) for i, val := range values { if val == nil { continue @@ -405,7 +416,14 @@ func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { if min > max { min, max = max, min } - sw.cols += fmt.Sprintf(``, min, max, width) + + sw.cols.WriteString(``) return nil } @@ -515,14 +533,24 @@ func setCellIntFunc(c *xlsxC, val interface{}) (err error) { func writeCell(buf *bufferedWriter, c xlsxC) { _, _ = buf.WriteString(``) if c.F != nil { @@ -549,8 +577,10 @@ func writeCell(buf *bufferedWriter, c xlsxC) { func (sw *StreamWriter) writeSheetData() { if !sw.sheetWritten { bulkAppendFields(&sw.rawData, sw.worksheet, 4, 5) - if len(sw.cols) > 0 { - _, _ = sw.rawData.WriteString("" + sw.cols + "") + if sw.cols.Len() > 0 { + _, _ = sw.rawData.WriteString("") + _, _ = sw.rawData.WriteString(sw.cols.String()) + _, _ = sw.rawData.WriteString("") } _, _ = sw.rawData.WriteString(``) sw.sheetWritten = true @@ -642,7 +672,7 @@ func (bw *bufferedWriter) Sync() (err error) { return nil } if bw.tmp == nil { - bw.tmp, err = ioutil.TempFile(os.TempDir(), "excelize-") + bw.tmp, err = os.CreateTemp(os.TempDir(), "excelize-") if err != nil { // can not use local storage return nil diff --git a/stream_test.go b/stream_test.go index 91aa58099e..c399d63391 100644 --- a/stream_test.go +++ b/stream_test.go @@ -3,7 +3,6 @@ package excelize import ( "encoding/xml" "fmt" - "io/ioutil" "math/rand" "os" "path/filepath" @@ -95,7 +94,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, streamWriter.rawData.Close()) assert.Error(t, streamWriter.Flush()) - streamWriter.rawData.tmp, err = ioutil.TempFile(os.TempDir(), "excelize-") + streamWriter.rawData.tmp, err = os.CreateTemp(os.TempDir(), "excelize-") assert.NoError(t, err) _, err = streamWriter.rawData.Reader() assert.NoError(t, err) From 3d02726ad4dc3bc6a92d5b68ef8421ac4db44076 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 14 Oct 2022 00:48:16 +0800 Subject: [PATCH 116/213] This closes #320, support custom chart axis font style --- chart.go | 19 ++++++++++++++++--- chart_test.go | 2 +- drawing.go | 22 +++++++++++++++++----- file.go | 4 ++-- shape.go | 28 +++------------------------- styles.go | 18 ++++++------------ table.go | 2 +- xmlChart.go | 45 ++++++++++++++++++++------------------------- xmlDrawing.go | 14 ++++++++++++-- 9 files changed, 78 insertions(+), 76 deletions(-) diff --git a/chart.go b/chart.go index 0caa505480..ce11b595a8 100644 --- a/chart.go +++ b/chart.go @@ -750,22 +750,24 @@ func parseChartOptions(opts string) (*chartOptions, error) { // reverse_order // maximum // minimum +// number_font // // The properties of y_axis that can be set are: // // none // major_grid_lines // minor_grid_lines -// major_unit +// tick_label_skip // reverse_order // maximum // minimum +// number_font // // none: Disable axes. // -// major_grid_lines: Specifies major gridlines. +// major_grid_lines: Specifies major grid lines. // -// minor_grid_lines: Specifies minor gridlines. +// minor_grid_lines: Specifies minor grid lines. // // major_unit: Specifies the distance between major ticks. Shall contain a positive floating-point number. The major_unit property is optional. The default value is auto. // @@ -777,6 +779,17 @@ func parseChartOptions(opts string) (*chartOptions, error) { // // minimum: Specifies that the fixed minimum, 0 is auto. The minimum property is optional. The default value is auto. // +// number_font: Specifies that the font of the horizontal and vertical axis. The properties of number_font that can be set are: +// +// bold +// italic +// underline +// family +// size +// strike +// color +// vertAlign +// // Set chart size by dimension property. The dimension property is optional. The default width is 480, and height is 290. // // combo: Specifies the create a chart that combines two or more chart types diff --git a/chart_test.go b/chart_test.go index 3c80b390ea..a0f7156618 100644 --- a/chart_test.go +++ b/chart_test.go @@ -116,7 +116,7 @@ func TestAddChart(t *testing.T) { // Test add chart on not exists worksheet. assert.EqualError(t, f.AddChart("SheetN", "P1", "{}"), "sheet SheetN does not exist") - assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"none":true,"show_legend_key":true},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) + assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"none":true,"show_legend_key":true},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"number_font":{"bold":true,"italic":true,"underline":"dbl","color":"#000000"}},"y_axis":{"number_font":{"bold":false,"italic":false,"underline":"sng","color":"#777777"}}}`)) assert.NoError(t, f.AddChart("Sheet1", "X1", `{"type":"colStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet1", "P16", `{"type":"colPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"100% Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet1", "X16", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"3D Clustered Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) diff --git a/drawing.go b/drawing.go index 2da2573bc3..974d627507 100644 --- a/drawing.go +++ b/drawing.go @@ -1017,7 +1017,7 @@ func (f *File) drawPlotAreaCatAx(opts *chartOptions) []*cAxs { MinorTickMark: &attrValString{Val: stringPtr("none")}, TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), - TxPr: f.drawPlotAreaTxPr(), + TxPr: f.drawPlotAreaTxPr(&opts.YAxis), CrossAx: &attrValInt{Val: intPtr(753999904)}, Crosses: &attrValString{Val: stringPtr("autoZero")}, Auto: &attrValBool{Val: boolPtr(true)}, @@ -1071,7 +1071,7 @@ func (f *File) drawPlotAreaValAx(opts *chartOptions) []*cAxs { MinorTickMark: &attrValString{Val: stringPtr("none")}, TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), - TxPr: f.drawPlotAreaTxPr(), + TxPr: f.drawPlotAreaTxPr(&opts.XAxis), CrossAx: &attrValInt{Val: intPtr(754001152)}, Crosses: &attrValString{Val: stringPtr("autoZero")}, CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, @@ -1114,7 +1114,7 @@ func (f *File) drawPlotAreaSerAx(opts *chartOptions) []*cAxs { AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])}, TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), - TxPr: f.drawPlotAreaTxPr(), + TxPr: f.drawPlotAreaTxPr(nil), CrossAx: &attrValInt{Val: intPtr(753999904)}, }, } @@ -1140,8 +1140,8 @@ func (f *File) drawPlotAreaSpPr() *cSpPr { } // drawPlotAreaTxPr provides a function to draw the c:txPr element. -func (f *File) drawPlotAreaTxPr() *cTxPr { - return &cTxPr{ +func (f *File) drawPlotAreaTxPr(opts *chartAxisOptions) *cTxPr { + cTxPr := &cTxPr{ BodyPr: aBodyPr{ Rot: -60000000, SpcFirstLastPara: true, @@ -1176,6 +1176,18 @@ func (f *File) drawPlotAreaTxPr() *cTxPr { EndParaRPr: &aEndParaRPr{Lang: "en-US"}, }, } + if opts != nil { + cTxPr.P.PPr.DefRPr.B = opts.NumFont.Bold + cTxPr.P.PPr.DefRPr.I = opts.NumFont.Italic + if idx := inStrSlice(supportedDrawingUnderlineTypes, opts.NumFont.Underline, true); idx != -1 { + cTxPr.P.PPr.DefRPr.U = supportedDrawingUnderlineTypes[idx] + } + if opts.NumFont.Color != "" { + cTxPr.P.PPr.DefRPr.SolidFill.SchemeClr = nil + cTxPr.P.PPr.DefRPr.SolidFill.SrgbClr = &attrValString{Val: stringPtr(strings.ReplaceAll(strings.ToUpper(opts.NumFont.Color), "#", ""))} + } + } + return cTxPr } // drawingParser provides a function to parse drawingXML. In order to solve diff --git a/file.go b/file.go index c83d17efc4..7ce536c86e 100644 --- a/file.go +++ b/file.go @@ -72,7 +72,7 @@ func (f *File) SaveAs(name string, opts ...Options) error { return ErrMaxFilePathLength } f.Path = name - if _, ok := supportedContentType[filepath.Ext(f.Path)]; !ok { + if _, ok := supportedContentTypes[filepath.Ext(f.Path)]; !ok { return ErrWorkbookFileFormat } file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) @@ -112,7 +112,7 @@ func (f *File) WriteTo(w io.Writer, opts ...Options) (int64, error) { f.options = &opts[i] } if len(f.Path) != 0 { - contentType, ok := supportedContentType[filepath.Ext(f.Path)] + contentType, ok := supportedContentTypes[filepath.Ext(f.Path)] if !ok { return 0, ErrWorkbookFileFormat } diff --git a/shape.go b/shape.go index eca354f50d..e3c6c8bc17 100644 --- a/shape.go +++ b/shape.go @@ -323,27 +323,6 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOption colIdx := fromCol - 1 rowIdx := fromRow - 1 - textUnderlineType := map[string]bool{ - "none": true, - "words": true, - "sng": true, - "dbl": true, - "heavy": true, - "dotted": true, - "dottedHeavy": true, - "dash": true, - "dashHeavy": true, - "dashLong": true, - "dashLongHeavy": true, - "dotDash": true, - "dotDashHeavy": true, - "dotDotDash": true, - "dotDotDashHeavy": true, - "wavy": true, - "wavyHeavy": true, - "wavyDbl": true, - } - width := int(float64(opts.Width) * opts.Format.XScale) height := int(float64(opts.Height) * opts.Format.YScale) @@ -422,10 +401,9 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOption } } for _, p := range opts.Paragraph { - u := p.Font.Underline - _, ok := textUnderlineType[u] - if !ok { - u = "none" + u := "none" + if idx := inStrSlice(supportedDrawingUnderlineTypes, p.Font.Underline, true); idx != -1 { + u = supportedDrawingUnderlineTypes[idx] } text := p.Text if text == "" { diff --git a/styles.go b/styles.go index 5299fbd58b..dc34427ef4 100644 --- a/styles.go +++ b/styles.go @@ -2087,7 +2087,6 @@ func (f *File) getFontID(styleSheet *xlsxStyleSheet, style *Style) (fontID int) // newFont provides a function to add font style by given cell format // settings. func (f *File) newFont(style *Style) *xlsxFont { - fontUnderlineType := map[string]string{"single": "single", "double": "double"} if style.Font.Size < MinFontSize { style.Font.Size = 11 } @@ -2112,9 +2111,8 @@ func (f *File) newFont(style *Style) *xlsxFont { if style.Font.Strike { fnt.Strike = &attrValBool{Val: &style.Font.Strike} } - val, ok := fontUnderlineType[style.Font.Underline] - if ok { - fnt.U = &attrValString{Val: stringPtr(val)} + if idx := inStrSlice(supportedUnderlineTypes, style.Font.Underline, true); idx != -1 { + fnt.U = &attrValString{Val: stringPtr(supportedUnderlineTypes[idx])} } return &fnt } @@ -3100,13 +3098,10 @@ func drawCondFmtCellIs(p int, ct string, format *conditionalOptions) *xlsxCfRule DxfID: &format.Format, } // "between" and "not between" criteria require 2 values. - _, ok := map[string]bool{"between": true, "notBetween": true}[ct] - if ok { - c.Formula = append(c.Formula, format.Minimum) - c.Formula = append(c.Formula, format.Maximum) + if ct == "between" || ct == "notBetween" { + c.Formula = append(c.Formula, []string{format.Minimum, format.Maximum}...) } - _, ok = map[string]bool{"equal": true, "notEqual": true, "greaterThan": true, "lessThan": true, "greaterThanOrEqual": true, "lessThanOrEqual": true, "containsText": true, "notContains": true, "beginsWith": true, "endsWith": true}[ct] - if ok { + if idx := inStrSlice([]string{"equal", "notEqual", "greaterThan", "lessThan", "greaterThanOrEqual", "lessThanOrEqual", "containsText", "notContains", "beginsWith", "endsWith"}, ct, true); idx != -1 { c.Formula = append(c.Formula, format.Value) } return c @@ -3124,8 +3119,7 @@ func drawCondFmtTop10(p int, ct string, format *conditionalOptions) *xlsxCfRule DxfID: &format.Format, Percent: format.Percent, } - rank, err := strconv.Atoi(format.Value) - if err == nil { + if rank, err := strconv.Atoi(format.Value); err == nil { c.Rank = rank } return c diff --git a/table.go b/table.go index f7cac2097a..112882c235 100644 --- a/table.go +++ b/table.go @@ -516,7 +516,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str } } } - // if the string token contains an Excel match character then change the + // If the string token contains an Excel match character then change the // operator type to indicate a non "simple" equality. re, _ = regexp.Match("[*?]", []byte(token)) if operator == 2 && re { diff --git a/xmlChart.go b/xmlChart.go index 9024770b02..27a790eae7 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -521,31 +521,26 @@ type cPageMargins struct { // chartAxisOptions directly maps the format settings of the chart axis. type chartAxisOptions struct { - None bool `json:"none"` - Crossing string `json:"crossing"` - MajorGridlines bool `json:"major_grid_lines"` - MinorGridlines bool `json:"minor_grid_lines"` - MajorTickMark string `json:"major_tick_mark"` - MinorTickMark string `json:"minor_tick_mark"` - MinorUnitType string `json:"minor_unit_type"` - MajorUnit float64 `json:"major_unit"` - MajorUnitType string `json:"major_unit_type"` - TickLabelSkip int `json:"tick_label_skip"` - DisplayUnits string `json:"display_units"` - DisplayUnitsVisible bool `json:"display_units_visible"` - DateAxis bool `json:"date_axis"` - ReverseOrder bool `json:"reverse_order"` - Maximum *float64 `json:"maximum"` - Minimum *float64 `json:"minimum"` - NumFormat string `json:"num_format"` - NumFont struct { - Color string `json:"color"` - Bold bool `json:"bold"` - Italic bool `json:"italic"` - Underline bool `json:"underline"` - } `json:"num_font"` - LogBase float64 `json:"logbase"` - NameLayout layoutOptions `json:"name_layout"` + None bool `json:"none"` + Crossing string `json:"crossing"` + MajorGridlines bool `json:"major_grid_lines"` + MinorGridlines bool `json:"minor_grid_lines"` + MajorTickMark string `json:"major_tick_mark"` + MinorTickMark string `json:"minor_tick_mark"` + MinorUnitType string `json:"minor_unit_type"` + MajorUnit float64 `json:"major_unit"` + MajorUnitType string `json:"major_unit_type"` + TickLabelSkip int `json:"tick_label_skip"` + DisplayUnits string `json:"display_units"` + DisplayUnitsVisible bool `json:"display_units_visible"` + DateAxis bool `json:"date_axis"` + ReverseOrder bool `json:"reverse_order"` + Maximum *float64 `json:"maximum"` + Minimum *float64 `json:"minimum"` + NumFormat string `json:"number_format"` + NumFont Font `json:"number_font"` + LogBase float64 `json:"logbase"` + NameLayout layoutOptions `json:"name_layout"` } // chartDimensionOptions directly maps the dimension of the chart. diff --git a/xmlDrawing.go b/xmlDrawing.go index 6a2f79ddf5..b52e44970b 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -144,8 +144,8 @@ const ( // supportedImageTypes defined supported image types. var supportedImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff", ".emf": ".emf", ".wmf": ".wmf", ".emz": ".emz", ".wmz": ".wmz"} -// supportedContentType defined supported file format types. -var supportedContentType = map[string]string{ +// supportedContentTypes defined supported file format types. +var supportedContentTypes = map[string]string{ ".xlam": ContentTypeAddinMacro, ".xlsm": ContentTypeMacro, ".xlsx": ContentTypeSheetML, @@ -153,6 +153,16 @@ var supportedContentType = map[string]string{ ".xltx": ContentTypeTemplate, } +// supportedUnderlineTypes defined supported underline types. +var supportedUnderlineTypes = []string{"none", "single", "double"} + +// supportedDrawingUnderlineTypes defined supported underline types in drawing +// markup language. +var supportedDrawingUnderlineTypes = []string{ + "none", "words", "sng", "dbl", "heavy", "dotted", "dottedHeavy", "dash", "dashHeavy", "dashLong", "dashLongHeavy", "dotDash", "dotDashHeavy", "dotDotDash", "dotDotDashHeavy", "wavy", "wavyHeavy", + "wavyDbl", +} + // xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This // element specifies non-visual canvas properties. This allows for additional // information that does not affect the appearance of the picture to be stored. From 3ece904b0082f4d63afe0d795b61c860d0790c83 Mon Sep 17 00:00:00 2001 From: GaoFei Date: Sat, 15 Oct 2022 00:03:49 +0800 Subject: [PATCH 117/213] This closes #1369, support set, and get font color with theme and tint (#1370) --- cell.go | 8 +++++--- cell_test.go | 22 ++++++++++++++-------- chart.go | 9 +++++---- chart_test.go | 2 +- drawing.go | 10 +++++----- styles.go | 29 +++++++++++++++++++++++++---- xmlChart.go | 4 ++-- xmlStyles.go | 18 ++++++++++-------- 8 files changed, 67 insertions(+), 35 deletions(-) diff --git a/cell.go b/cell.go index 80eb035268..550ca3879c 100644 --- a/cell.go +++ b/cell.go @@ -823,6 +823,10 @@ func getCellRichText(si *xlsxSI) (runs []RichTextRun) { font.Strike = v.RPr.Strike != nil if v.RPr.Color != nil { font.Color = strings.TrimPrefix(v.RPr.Color.RGB, "FF") + if v.RPr.Color.Theme != nil { + font.ColorTheme = v.RPr.Color.Theme + } + font.ColorTint = v.RPr.Color.Tint } run.Font = &font } @@ -879,9 +883,7 @@ func newRpr(fnt *Font) *xlsxRPr { if fnt.Size > 0 { rpr.Sz = &attrValFloat{Val: &fnt.Size} } - if fnt.Color != "" { - rpr.Color = &xlsxColor{RGB: getPaletteColor(fnt.Color)} - } + rpr.Color = newFontColor(fnt) return &rpr } diff --git a/cell_test.go b/cell_test.go index 759805835b..511078ee54 100644 --- a/cell_test.go +++ b/cell_test.go @@ -518,7 +518,7 @@ func TestSetCellFormula(t *testing.T) { } func TestGetCellRichText(t *testing.T) { - f := NewFile() + f, theme := NewFile(), 1 runsSource := []RichTextRun{ { @@ -527,13 +527,15 @@ func TestGetCellRichText(t *testing.T) { { Text: "b", Font: &Font{ - Underline: "single", - Color: "ff0000", - Bold: true, - Italic: true, - Family: "Times New Roman", - Size: 100, - Strike: true, + Underline: "single", + Color: "ff0000", + ColorTheme: &theme, + ColorTint: 0.5, + Bold: true, + Italic: true, + Family: "Times New Roman", + Size: 100, + Strike: true, }, }, } @@ -580,6 +582,10 @@ func TestGetCellRichText(t *testing.T) { // Test set cell rich text with illegal cell reference _, err = f.GetCellRichText("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + // Test set rich text color theme without tint + assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTheme: &theme}}})) + // Test set rich text color tint without theme + assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTint: 0.5}}})) } func TestSetCellRichText(t *testing.T) { diff --git a/chart.go b/chart.go index ce11b595a8..be6ddd8f9f 100644 --- a/chart.go +++ b/chart.go @@ -702,7 +702,7 @@ func parseChartOptions(opts string) (*chartOptions, error) { // // title // -// name: Set the name (title) for the chart. The name is displayed above the chart. The name can also be a formula such as Sheet1!$A$1 or a list with a sheetname. The name property is optional. The default is to have no chart title. +// name: Set the name (title) for the chart. The name is displayed above the chart. The name can also be a formula such as Sheet1!$A$1 or a list with a sheet name. The name property is optional. The default is to have no chart title. // // Specifies how blank cells are plotted on the chart by show_blanks_as. The default value is gap. The options that can be set are: // @@ -750,18 +750,19 @@ func parseChartOptions(opts string) (*chartOptions, error) { // reverse_order // maximum // minimum -// number_font +// font // // The properties of y_axis that can be set are: // // none // major_grid_lines // minor_grid_lines +// major_unit // tick_label_skip // reverse_order // maximum // minimum -// number_font +// font // // none: Disable axes. // @@ -779,7 +780,7 @@ func parseChartOptions(opts string) (*chartOptions, error) { // // minimum: Specifies that the fixed minimum, 0 is auto. The minimum property is optional. The default value is auto. // -// number_font: Specifies that the font of the horizontal and vertical axis. The properties of number_font that can be set are: +// font: Specifies that the font of the horizontal and vertical axis. The properties of font that can be set are: // // bold // italic diff --git a/chart_test.go b/chart_test.go index a0f7156618..6d40b44f93 100644 --- a/chart_test.go +++ b/chart_test.go @@ -116,7 +116,7 @@ func TestAddChart(t *testing.T) { // Test add chart on not exists worksheet. assert.EqualError(t, f.AddChart("SheetN", "P1", "{}"), "sheet SheetN does not exist") - assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"none":true,"show_legend_key":true},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"number_font":{"bold":true,"italic":true,"underline":"dbl","color":"#000000"}},"y_axis":{"number_font":{"bold":false,"italic":false,"underline":"sng","color":"#777777"}}}`)) + assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"none":true,"show_legend_key":true},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"font":{"bold":true,"italic":true,"underline":"dbl","color":"#000000"}},"y_axis":{"font":{"bold":false,"italic":false,"underline":"sng","color":"#777777"}}}`)) assert.NoError(t, f.AddChart("Sheet1", "X1", `{"type":"colStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet1", "P16", `{"type":"colPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"100% Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet1", "X16", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"3D Clustered Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) diff --git a/drawing.go b/drawing.go index 974d627507..0bd8604b2a 100644 --- a/drawing.go +++ b/drawing.go @@ -1177,14 +1177,14 @@ func (f *File) drawPlotAreaTxPr(opts *chartAxisOptions) *cTxPr { }, } if opts != nil { - cTxPr.P.PPr.DefRPr.B = opts.NumFont.Bold - cTxPr.P.PPr.DefRPr.I = opts.NumFont.Italic - if idx := inStrSlice(supportedDrawingUnderlineTypes, opts.NumFont.Underline, true); idx != -1 { + cTxPr.P.PPr.DefRPr.B = opts.Font.Bold + cTxPr.P.PPr.DefRPr.I = opts.Font.Italic + if idx := inStrSlice(supportedDrawingUnderlineTypes, opts.Font.Underline, true); idx != -1 { cTxPr.P.PPr.DefRPr.U = supportedDrawingUnderlineTypes[idx] } - if opts.NumFont.Color != "" { + if opts.Font.Color != "" { cTxPr.P.PPr.DefRPr.SolidFill.SchemeClr = nil - cTxPr.P.PPr.DefRPr.SolidFill.SrgbClr = &attrValString{Val: stringPtr(strings.ReplaceAll(strings.ToUpper(opts.NumFont.Color), "#", ""))} + cTxPr.P.PPr.DefRPr.SolidFill.SrgbClr = &attrValString{Val: stringPtr(strings.ReplaceAll(strings.ToUpper(opts.Font.Color), "#", ""))} } } return cTxPr diff --git a/styles.go b/styles.go index dc34427ef4..b4d0a53d44 100644 --- a/styles.go +++ b/styles.go @@ -2084,21 +2084,42 @@ func (f *File) getFontID(styleSheet *xlsxStyleSheet, style *Style) (fontID int) return } +// newFontColor set font color by given styles. +func newFontColor(font *Font) *xlsxColor { + var fontColor *xlsxColor + prepareFontColor := func() { + if fontColor != nil { + return + } + fontColor = &xlsxColor{} + } + if font.Color != "" { + prepareFontColor() + fontColor.RGB = getPaletteColor(font.Color) + } + if font.ColorTheme != nil { + prepareFontColor() + fontColor.Theme = font.ColorTheme + } + if font.ColorTint != 0 { + prepareFontColor() + fontColor.Tint = font.ColorTint + } + return fontColor +} + // newFont provides a function to add font style by given cell format // settings. func (f *File) newFont(style *Style) *xlsxFont { if style.Font.Size < MinFontSize { style.Font.Size = 11 } - if style.Font.Color == "" { - style.Font.Color = "#000000" - } fnt := xlsxFont{ Sz: &attrValFloat{Val: float64Ptr(style.Font.Size)}, - Color: &xlsxColor{RGB: getPaletteColor(style.Font.Color)}, Name: &attrValString{Val: stringPtr(style.Font.Family)}, Family: &attrValInt{Val: intPtr(2)}, } + fnt.Color = newFontColor(style.Font) if style.Font.Bold { fnt.B = &attrValBool{Val: &style.Font.Bold} } diff --git a/xmlChart.go b/xmlChart.go index 27a790eae7..2ebcdefe67 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -476,7 +476,7 @@ type cNumCache struct { PtCount *attrValInt `xml:"ptCount"` } -// cDLbls (Data Lables) directly maps the dLbls element. This element serves +// cDLbls (Data Labels) directly maps the dLbls element. This element serves // as a root element that specifies the settings for the data labels for an // entire series or the entire chart. It contains child elements that specify // the specific formatting and positioning settings. @@ -538,7 +538,7 @@ type chartAxisOptions struct { Maximum *float64 `json:"maximum"` Minimum *float64 `json:"minimum"` NumFormat string `json:"number_format"` - NumFont Font `json:"number_font"` + Font Font `json:"font"` LogBase float64 `json:"logbase"` NameLayout layoutOptions `json:"name_layout"` } diff --git a/xmlStyles.go b/xmlStyles.go index 0000d45a52..e35dbddc93 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -334,14 +334,16 @@ type Border struct { // Font directly maps the font settings of the fonts. type Font struct { - Bold bool `json:"bold"` - Italic bool `json:"italic"` - Underline string `json:"underline"` - Family string `json:"family"` - Size float64 `json:"size"` - Strike bool `json:"strike"` - Color string `json:"color"` - VertAlign string `json:"vertAlign"` + Bold bool `json:"bold"` + Italic bool `json:"italic"` + Underline string `json:"underline"` + Family string `json:"family"` + Size float64 `json:"size"` + Strike bool `json:"strike"` + Color string `json:"color"` + ColorTheme *int `json:"color_theme"` + ColorTint float64 `json:"color_tint"` + VertAlign string `json:"vertAlign"` } // Fill directly maps the fill settings of the cells. From 2df615fa2831bd578371d4e3606f16461c474ce7 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 20 Oct 2022 00:02:30 +0800 Subject: [PATCH 118/213] This close #1373, fixes the incorrect build-in number format apply the result - An error will be returned when setting the stream row without ascending row numbers, to avoid potential mistakes as mentioned in #1139 - Updated unit tests --- errors.go | 6 ++++ excelize_test.go | 10 +++--- rows_test.go | 63 ++++++++++++++++++++++++++++++++++++++ stream.go | 7 ++++- stream_test.go | 7 +++-- styles.go | 79 ++++++++++++++++++++++++++++++++++++++++++++---- 6 files changed, 158 insertions(+), 14 deletions(-) diff --git a/errors.go b/errors.go index fd896a6303..6a23a2e954 100644 --- a/errors.go +++ b/errors.go @@ -87,6 +87,12 @@ func newDecodeXMLError(err error) error { return fmt.Errorf("xml decode error: %s", err) } +// newStreamSetRowError defined the error message on the stream writer +// receiving the non-ascending row number. +func newStreamSetRowError(row int) error { + return fmt.Errorf("row %d has already been written", row) +} + var ( // ErrStreamSetColWidth defined the error message on set column width in // stream writing mode. diff --git a/excelize_test.go b/excelize_test.go index 12d155d6c4..4c86d56005 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -721,10 +721,10 @@ func TestSetCellStyleNumberFormat(t *testing.T) { data := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49} value := []string{"37947.7500001", "-37947.7500001", "0.007", "2.1", "String"} expected := [][]string{ - {"37947.7500001", "37948", "37947.75", "37948", "37947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 pm", "6:00:00 pm", "18:00", "18:00:00", "11/22/03 18:00", "37947", "37947", "37947.75", "37947.75", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "37947.7500001", "3.79E+04", "37947.7500001"}, - {"-37947.7500001", "-37948", "-37947.75", "-37948", "-37947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37947)", "(37947)", "(-37947.75)", "(-37947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-3.79E+04", "-37947.7500001"}, - {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 am", "0:10:04 am", "00:10", "00:10:04", "12/30/99 00:10", "0", "0", "0.01", "0.01", "0.007", "0.007", "0.007", "0.007", "10:04", "0:10:04", "0.007", "7.00E-03", "0.007"}, - {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 am", "2:24:00 am", "02:24", "02:24:00", "1/1/00 02:24", "2", "2", "2.10", "2.10", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "2.1", "2.10E+00", "2.1"}, + {"37947.7500001", "37948", "37947.75", "37,948", "37947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 pm", "6:00:00 pm", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "37947.7500001", "3.79E+04", "37947.7500001"}, + {"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-3.79E+04", "-37947.7500001"}, + {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 am", "0:10:04 am", "00:10", "00:10:04", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:04", "0:10:04", "0.007", "7.00E-03", "0.007"}, + {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 am", "2:24:00 am", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "2.1", "2.10E+00", "2.1"}, {"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String"}, } @@ -744,7 +744,7 @@ func TestSetCellStyleNumberFormat(t *testing.T) { } assert.NoError(t, f.SetCellStyle("Sheet2", c, c, style)) cellValue, err := f.GetCellValue("Sheet2", c) - assert.Equal(t, expected[i][k], cellValue) + assert.Equal(t, expected[i][k], cellValue, "Sheet2!"+c, i, k) assert.NoError(t, err) } } diff --git a/rows_test.go b/rows_test.go index 74c4d25ffd..76823bad65 100644 --- a/rows_test.go +++ b/rows_test.go @@ -993,6 +993,68 @@ func TestNumberFormats(t *testing.T) { } assert.Equal(t, []string{"", "200", "450", "200", "510", "315", "127", "89", "348", "53", "37"}, cells[3]) assert.NoError(t, f.Close()) + + f = NewFile() + numFmt1, err := f.NewStyle(&Style{NumFmt: 1}) + assert.NoError(t, err) + numFmt2, err := f.NewStyle(&Style{NumFmt: 2}) + assert.NoError(t, err) + numFmt3, err := f.NewStyle(&Style{NumFmt: 3}) + assert.NoError(t, err) + numFmt9, err := f.NewStyle(&Style{NumFmt: 9}) + assert.NoError(t, err) + numFmt10, err := f.NewStyle(&Style{NumFmt: 10}) + assert.NoError(t, err) + numFmt37, err := f.NewStyle(&Style{NumFmt: 37}) + assert.NoError(t, err) + numFmt38, err := f.NewStyle(&Style{NumFmt: 38}) + assert.NoError(t, err) + numFmt39, err := f.NewStyle(&Style{NumFmt: 39}) + assert.NoError(t, err) + numFmt40, err := f.NewStyle(&Style{NumFmt: 40}) + assert.NoError(t, err) + for _, cases := range [][]interface{}{ + {"A1", numFmt1, 8.8888666665555493e+19, "88888666665555500000"}, + {"A2", numFmt1, 8.8888666665555487, "9"}, + {"A3", numFmt2, 8.8888666665555493e+19, "88888666665555500000.00"}, + {"A4", numFmt2, 8.8888666665555487, "8.89"}, + {"A5", numFmt3, 8.8888666665555493e+19, "88,888,666,665,555,500,000"}, + {"A6", numFmt3, 8.8888666665555487, "9"}, + {"A7", numFmt3, 123, "123"}, + {"A8", numFmt3, -1234, "-1,234"}, + {"A9", numFmt9, 8.8888666665555493e+19, "8888866666555550000000%"}, + {"A10", numFmt9, -8.8888666665555493e+19, "-8888866666555550000000%"}, + {"A11", numFmt9, 8.8888666665555487, "889%"}, + {"A12", numFmt9, -8.8888666665555487, "-889%"}, + {"A13", numFmt10, 8.8888666665555493e+19, "8888866666555550000000.00%"}, + {"A14", numFmt10, -8.8888666665555493e+19, "-8888866666555550000000.00%"}, + {"A15", numFmt10, 8.8888666665555487, "888.89%"}, + {"A16", numFmt10, -8.8888666665555487, "-888.89%"}, + {"A17", numFmt37, 8.8888666665555493e+19, "88,888,666,665,555,500,000 "}, + {"A18", numFmt37, -8.8888666665555493e+19, "(88,888,666,665,555,500,000)"}, + {"A19", numFmt37, 8.8888666665555487, "9 "}, + {"A20", numFmt37, -8.8888666665555487, "(9)"}, + {"A21", numFmt38, 8.8888666665555493e+19, "88,888,666,665,555,500,000 "}, + {"A22", numFmt38, -8.8888666665555493e+19, "(88,888,666,665,555,500,000)"}, + {"A23", numFmt38, 8.8888666665555487, "9 "}, + {"A24", numFmt38, -8.8888666665555487, "(9)"}, + {"A25", numFmt39, 8.8888666665555493e+19, "88,888,666,665,555,500,000.00 "}, + {"A26", numFmt39, -8.8888666665555493e+19, "(88,888,666,665,555,500,000.00)"}, + {"A27", numFmt39, 8.8888666665555487, "8.89 "}, + {"A28", numFmt39, -8.8888666665555487, "(8.89)"}, + {"A29", numFmt40, 8.8888666665555493e+19, "88,888,666,665,555,500,000.00 "}, + {"A30", numFmt40, -8.8888666665555493e+19, "(88,888,666,665,555,500,000.00)"}, + {"A31", numFmt40, 8.8888666665555487, "8.89 "}, + {"A32", numFmt40, -8.8888666665555487, "(8.89)"}, + } { + cell, styleID, value, expected := cases[0].(string), cases[1].(int), cases[2], cases[3].(string) + f.SetCellStyle("Sheet1", cell, cell, styleID) + assert.NoError(t, f.SetCellValue("Sheet1", cell, value)) + result, err := f.GetCellValue("Sheet1", cell) + assert.NoError(t, err) + assert.Equal(t, expected, result) + } + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx"))) } func BenchmarkRows(b *testing.B) { @@ -1016,6 +1078,7 @@ func BenchmarkRows(b *testing.B) { } } +// trimSliceSpace trim continually blank element in the tail of slice. func trimSliceSpace(s []string) []string { for { if len(s) > 0 && s[len(s)-1] == "" { diff --git a/stream.go b/stream.go index 62470b59bb..44d8eb710f 100644 --- a/stream.go +++ b/stream.go @@ -32,6 +32,7 @@ type StreamWriter struct { cols strings.Builder worksheet *xlsxWorksheet rawData bufferedWriter + rows int mergeCellsCount int mergeCells strings.Builder tableParts string @@ -40,7 +41,7 @@ type StreamWriter struct { // NewStreamWriter return stream writer struct by given worksheet name for // generate new worksheet with large amounts of data. Note that after set // rows, you must call the 'Flush' method to end the streaming writing process -// and ensure that the order of line numbers is ascending, the normal mode +// and ensure that the order of row numbers is ascending, the normal mode // functions and stream mode functions can't be work mixed to writing data on // the worksheets, you can't get cell value when in-memory chunks data over // 16MB. For example, set data for worksheet of size 102400 rows x 50 columns @@ -358,6 +359,10 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt if err != nil { return err } + if row <= sw.rows { + return newStreamSetRowError(row) + } + sw.rows = row sw.writeSheetData() options := parseRowOpts(opts...) attrs, err := options.marshalAttrs() diff --git a/stream_test.go b/stream_test.go index c399d63391..a4a0590aae 100644 --- a/stream_test.go +++ b/stream_test.go @@ -61,7 +61,7 @@ func TestStreamWriter(t *testing.T) { }})) assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()})) assert.NoError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: 20, Hidden: true, StyleID: styleID})) - assert.EqualError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: MaxRowHeight + 1}), ErrMaxRowHeight.Error()) + assert.EqualError(t, streamWriter.SetRow("A8", nil, RowOpts{Height: MaxRowHeight + 1}), ErrMaxRowHeight.Error()) for rowID := 10; rowID <= 51200; rowID++ { row := make([]interface{}, 50) @@ -77,7 +77,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) // Test set cell column overflow. - assert.ErrorIs(t, streamWriter.SetRow("XFD1", []interface{}{"A", "B", "C"}), ErrColumnNumber) + assert.ErrorIs(t, streamWriter.SetRow("XFD51201", []interface{}{"A", "B", "C"}), ErrColumnNumber) // Test close temporary file error. file = NewFile() @@ -226,6 +226,9 @@ func TestStreamSetRow(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + // Test set row with non-ascending row number + assert.NoError(t, streamWriter.SetRow("A1", []interface{}{})) + assert.EqualError(t, streamWriter.SetRow("A1", []interface{}{}), newStreamSetRowError(1).Error()) } func TestStreamSetRowNilValues(t *testing.T) { diff --git a/styles.go b/styles.go index b4d0a53d44..bbb21dcc50 100644 --- a/styles.go +++ b/styles.go @@ -758,7 +758,7 @@ var builtInNumFmtFunc = map[int]func(v, format string, date1904 bool) string{ 0: format, 1: formatToInt, 2: formatToFloat, - 3: formatToInt, + 3: formatToIntSeparator, 4: formatToFloat, 9: formatToC, 10: formatToD, @@ -869,6 +869,26 @@ var operatorType = map[string]string{ "greaterThanOrEqual": "greater than or equal to", } +// printCommaSep format number with thousands separator. +func printCommaSep(text string) string { + var ( + target strings.Builder + subStr = strings.Split(text, ".") + length = len(subStr[0]) + ) + for i := 0; i < length; i++ { + if i > 0 && (length-i)%3 == 0 { + target.WriteString(",") + } + target.WriteString(string(text[i])) + } + if len(subStr) == 2 { + target.WriteString(".") + target.WriteString(subStr[1]) + } + return target.String() +} + // formatToInt provides a function to convert original string to integer // format as string type by given built-in number formats code and cell // string. @@ -880,7 +900,7 @@ func formatToInt(v, format string, date1904 bool) string { if err != nil { return v } - return fmt.Sprintf("%d", int64(math.Round(f))) + return strconv.FormatFloat(math.Round(f), 'f', -1, 64) } // formatToFloat provides a function to convert original string to float @@ -894,9 +914,27 @@ func formatToFloat(v, format string, date1904 bool) string { if err != nil { return v } + source := strconv.FormatFloat(f, 'f', -1, 64) + if !strings.Contains(source, ".") { + return source + ".00" + } return fmt.Sprintf("%.2f", f) } +// formatToIntSeparator provides a function to convert original string to +// integer format as string type by given built-in number formats code and cell +// string. +func formatToIntSeparator(v, format string, date1904 bool) string { + if strings.Contains(v, "_") { + return v + } + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return v + } + return printCommaSep(strconv.FormatFloat(math.Round(f), 'f', -1, 64)) +} + // formatToA provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. func formatToA(v, format string, date1904 bool) string { @@ -907,10 +945,17 @@ func formatToA(v, format string, date1904 bool) string { if err != nil { return v } + var target strings.Builder if f < 0 { - return fmt.Sprintf("(%d)", int(math.Abs(f))) + target.WriteString("(") } - return fmt.Sprintf("%d", int(f)) + target.WriteString(printCommaSep(strconv.FormatFloat(math.Abs(math.Round(f)), 'f', -1, 64))) + if f < 0 { + target.WriteString(")") + } else { + target.WriteString(" ") + } + return target.String() } // formatToB provides a function to convert original string to special format @@ -923,10 +968,24 @@ func formatToB(v, format string, date1904 bool) string { if err != nil { return v } + var target strings.Builder if f < 0 { - return fmt.Sprintf("(%.2f)", f) + target.WriteString("(") } - return fmt.Sprintf("%.2f", f) + source := strconv.FormatFloat(math.Abs(f), 'f', -1, 64) + var text string + if !strings.Contains(source, ".") { + text = printCommaSep(source + ".00") + } else { + text = printCommaSep(fmt.Sprintf("%.2f", math.Abs(f))) + } + target.WriteString(text) + if f < 0 { + target.WriteString(")") + } else { + target.WriteString(" ") + } + return target.String() } // formatToC provides a function to convert original string to special format @@ -939,6 +998,10 @@ func formatToC(v, format string, date1904 bool) string { if err != nil { return v } + source := strconv.FormatFloat(f, 'f', -1, 64) + if !strings.Contains(source, ".") { + return source + "00%" + } return fmt.Sprintf("%.f%%", f*100) } @@ -952,6 +1015,10 @@ func formatToD(v, format string, date1904 bool) string { if err != nil { return v } + source := strconv.FormatFloat(f, 'f', -1, 64) + if !strings.Contains(source, ".") { + return source + "00.00%" + } return fmt.Sprintf("%.2f%%", f*100) } From f843a9ea56710deb4cdb77ea2cd3a08d8d82d3e6 Mon Sep 17 00:00:00 2001 From: gonghaibinx <116247046+gonghaibinx@users.noreply.github.com> Date: Fri, 21 Oct 2022 00:04:32 +0800 Subject: [PATCH 119/213] Fix the formula calculation result issue of the OR function (#1374) Co-authored-by: gonghaibin --- calc.go | 3 +++ calc_test.go | 1 + 2 files changed, 4 insertions(+) diff --git a/calc.go b/calc.go index 5d55992160..796ca169fd 100644 --- a/calc.go +++ b/calc.go @@ -11623,6 +11623,9 @@ func (fn *formulaFuncs) OR(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) case ArgNumber: or = token.Number != 0 + if or { + return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(or))) + } case ArgMatrix: // TODO return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) diff --git a/calc_test.go b/calc_test.go index df86f90743..ea3f014806 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1421,6 +1421,7 @@ func TestCalcCellValue(t *testing.T) { "=OR(0)": "FALSE", "=OR(1=2,2=2)": "TRUE", "=OR(1=2,2=3)": "FALSE", + "=OR(1=1,2=3)": "TRUE", "=OR(\"TRUE\",\"FALSE\")": "TRUE", // SWITCH "=SWITCH(1,1,\"A\",2,\"B\",3,\"C\",\"N\")": "A", From 14c6a198ce27b44fcce5447a2b757ce403ebb8fc Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 24 Oct 2022 00:02:22 +0800 Subject: [PATCH 120/213] 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 #65, new formula functions: AGGREGATE and SUBTOTAL --- calc.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++--- calc_test.go | 61 ++++++++++++++++++++++++++++++-- cell.go | 1 + cell_test.go | 90 +++++++++++++++++++++++++--------------------- file.go | 2 +- rows.go | 55 ++++++++++++++++++++++------- rows_test.go | 2 +- sheetpr.go | 26 ++++---------- sheetpr_test.go | 16 --------- stream.go | 14 ++++---- stream_test.go | 2 +- styles.go | 4 +++ table.go | 2 +- xmlDrawing.go | 20 +++++++++++ xmlStyles.go | 21 +++++------ 15 files changed, 294 insertions(+), 116 deletions(-) diff --git a/calc.go b/calc.go index 796ca169fd..c600aaa32b 100644 --- a/calc.go +++ b/calc.go @@ -339,6 +339,7 @@ type formulaFuncs struct { // ACOT // ACOTH // ADDRESS +// AGGREGATE // AMORDEGRC // AMORLINC // AND @@ -700,6 +701,7 @@ type formulaFuncs struct { // STDEVPA // STEYX // SUBSTITUTE +// SUBTOTAL // SUM // SUMIF // SUMIFS @@ -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] @@ -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) { @@ -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 { @@ -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: // @@ -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: // @@ -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: diff --git a/calc_test.go b/calc_test.go index ea3f014806..1a8b8c62ec 100644 --- a/calc_test.go +++ b/calc_test.go @@ -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", @@ -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", @@ -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!", @@ -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(), diff --git a/cell.go b/cell.go index 550ca3879c..3fcbb7b3a4 100644 --- a/cell.go +++ b/cell.go @@ -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 diff --git a/cell_test.go b/cell_test.go index 511078ee54..980058a30e 100644 --- a/cell_test.go +++ b/cell_test.go @@ -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, ` - 2422.3000000000002 - 2422.3000000000002 - 12.4 - 964 - 1101.5999999999999 - 275.39999999999998 - 68.900000000000006 - 44385.208333333336 - 5.0999999999999996 - 5.1100000000000003 - 5.0999999999999996 - 5.1109999999999998 - 5.1111000000000004 - 2422.012345678 - 2422.0123456789 - 12.012345678901 - 964 - 1101.5999999999999 - 275.39999999999998 - 68.900000000000006 - 8.8880000000000001E-2 - 4.0000000000000003e-5 - 2422.3000000000002 - 1101.5999999999999 - 275.39999999999998 - 68.900000000000006 - 1.1000000000000001 - 1234567890123_4 - 123456789_0123_4 - +0.0000000000000000002399999999999992E-4 - 7.2399999999999992E-2 -`))) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, ` + 2422.3000000000002 + 2422.3000000000002 + 12.4 + 964 + 1101.5999999999999 + 275.39999999999998 + 68.900000000000006 + 44385.208333333336 + 5.0999999999999996 + 5.1100000000000003 + 5.0999999999999996 + 5.1109999999999998 + 5.1111000000000004 + 2422.012345678 + 2422.0123456789 + 12.012345678901 + 964 + 1101.5999999999999 + 275.39999999999998 + 68.900000000000006 + 8.8880000000000001E-2 + 4.0000000000000003e-5 + 2422.3000000000002 + 1101.5999999999999 + 275.39999999999998 + 68.900000000000006 + 1.1000000000000001 + 1234567890123_4 + 123456789_0123_4 + +0.0000000000000000002399999999999992E-4 + 7.2399999999999992E-2 + 20200208T080910.123 + 20200208T080910,123 + 20221022T150529Z + 2022-10-22T15:05:29Z + 2020-07-10 15:00:00.000`))) 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", @@ -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) } @@ -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", }, }, { @@ -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) diff --git a/file.go b/file.go index 7ce536c86e..43a37dd294 100644 --- a/file.go +++ b/file.go @@ -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() diff --git a/rows.go b/rows.go index 1fd6825ba9..9f791cb80b 100644 --- a/rows.go +++ b/rows.go @@ -20,6 +20,8 @@ import ( "math" "os" "strconv" + "strings" + "time" "github.com/mohae/deepcopy" ) @@ -447,6 +449,39 @@ func (f *File) sharedStringsReader() *xlsxSST { return f.SharedStrings } +// getCellDate parse cell value which containing a boolean. +func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) { + if !raw { + if c.V == "1" { + return "TRUE", nil + } + if c.V == "0" { + return "FALSE", nil + } + } + return f.formattedValue(c.S, c.V, raw), nil +} + +// getCellDate parse cell value which contains a date in the ISO 8601 format. +func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) { + if !raw { + layout := "20060102T150405.999" + if strings.HasSuffix(c.V, "Z") { + layout = "20060102T150405Z" + if strings.Contains(c.V, "-") { + layout = "2006-01-02T15:04:05Z" + } + } else if strings.Contains(c.V, "-") { + layout = "2006-01-02 15:04:05Z" + } + if timestamp, err := time.Parse(layout, strings.ReplaceAll(c.V, ",", ".")); err == nil { + excelTime, _ := timeToExcelTime(timestamp, false) + c.V = strconv.FormatFloat(excelTime, 'G', 15, 64) + } + } + return f.formattedValue(c.S, c.V, raw), nil +} + // getValueFrom return a value from a column/row cell, this function is // intended to be used with for range on rows an argument with the spreadsheet // opened file. @@ -455,15 +490,9 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { defer f.Unlock() switch c.T { case "b": - if !raw { - if c.V == "1" { - return "TRUE", nil - } - if c.V == "0" { - return "FALSE", nil - } - } - return f.formattedValue(c.S, c.V, raw), nil + return c.getCellBool(f, raw) + case "d": + return c.getCellDate(f, raw) case "s": if c.V != "" { xlsxSI := 0 @@ -760,7 +789,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in // // // -// Noteice: this method could be very slow for large spreadsheets (more than +// Notice: this method could be very slow for large spreadsheets (more than // 3000 rows one sheet). func checkRow(ws *xlsxWorksheet) error { for rowIdx := range ws.SheetData.Row { @@ -793,7 +822,7 @@ func checkRow(ws *xlsxWorksheet) error { if colCount < lastCol { oldList := rowData.C - newlist := make([]xlsxC, 0, lastCol) + newList := make([]xlsxC, 0, lastCol) rowData.C = ws.SheetData.Row[rowIdx].C[:0] @@ -802,10 +831,10 @@ func checkRow(ws *xlsxWorksheet) error { if err != nil { return err } - newlist = append(newlist, xlsxC{R: cellName}) + newList = append(newList, xlsxC{R: cellName}) } - rowData.C = newlist + rowData.C = newList for colIdx := range oldList { colData := &oldList[colIdx] diff --git a/rows_test.go b/rows_test.go index 76823bad65..423932f8ac 100644 --- a/rows_test.go +++ b/rows_test.go @@ -1048,7 +1048,7 @@ func TestNumberFormats(t *testing.T) { {"A32", numFmt40, -8.8888666665555487, "(8.89)"}, } { cell, styleID, value, expected := cases[0].(string), cases[1].(int), cases[2], cases[3].(string) - f.SetCellStyle("Sheet1", cell, cell, styleID) + assert.NoError(t, f.SetCellStyle("Sheet1", cell, cell, styleID)) assert.NoError(t, f.SetCellValue("Sheet1", cell, value)) result, err := f.GetCellValue("Sheet1", cell) assert.NoError(t, err) diff --git a/sheetpr.go b/sheetpr.go index 3a805c479a..b0f3945121 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -80,24 +80,12 @@ func (f *File) GetPageMargins(sheet string) (PageLayoutMarginsOptions, error) { return opts, err } if ws.PageMargins != nil { - if ws.PageMargins.Bottom != 0 { - opts.Bottom = float64Ptr(ws.PageMargins.Bottom) - } - if ws.PageMargins.Footer != 0 { - opts.Footer = float64Ptr(ws.PageMargins.Footer) - } - if ws.PageMargins.Header != 0 { - opts.Header = float64Ptr(ws.PageMargins.Header) - } - if ws.PageMargins.Left != 0 { - opts.Left = float64Ptr(ws.PageMargins.Left) - } - if ws.PageMargins.Right != 0 { - opts.Right = float64Ptr(ws.PageMargins.Right) - } - if ws.PageMargins.Top != 0 { - opts.Top = float64Ptr(ws.PageMargins.Top) - } + opts.Bottom = float64Ptr(ws.PageMargins.Bottom) + opts.Footer = float64Ptr(ws.PageMargins.Footer) + opts.Header = float64Ptr(ws.PageMargins.Header) + opts.Left = float64Ptr(ws.PageMargins.Left) + opts.Right = float64Ptr(ws.PageMargins.Right) + opts.Top = float64Ptr(ws.PageMargins.Top) } if ws.PrintOptions != nil { opts.Horizontally = boolPtr(ws.PrintOptions.HorizontalCentered) @@ -106,7 +94,7 @@ func (f *File) GetPageMargins(sheet string) (PageLayoutMarginsOptions, error) { return opts, err } -// prepareSheetPr sheetPr element if which not exist. +// prepareSheetPr create sheetPr element which not exist. func (ws *xlsxWorksheet) prepareSheetPr() { if ws.SheetPr == nil { ws.SheetPr = new(xlsxSheetPr) diff --git a/sheetpr_test.go b/sheetpr_test.go index b4ee18dba2..d422e3f65b 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -1,7 +1,6 @@ package excelize import ( - "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -39,21 +38,6 @@ func TestGetPageMargins(t *testing.T) { assert.EqualError(t, err, "sheet SheetN does not exist") } -func TestDebug(t *testing.T) { - f := NewFile() - assert.NoError(t, f.SetSheetProps("Sheet1", nil)) - ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") - assert.True(t, ok) - ws.(*xlsxWorksheet).PageMargins = nil - ws.(*xlsxWorksheet).PrintOptions = nil - ws.(*xlsxWorksheet).SheetPr = nil - ws.(*xlsxWorksheet).SheetFormatPr = nil - // w := uint8(10) - // f.SetSheetProps("Sheet1", &SheetPropsOptions{BaseColWidth: &w}) - f.SetPageMargins("Sheet1", &PageLayoutMarginsOptions{Horizontally: boolPtr(true)}) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDebug.xlsx"))) -} - func TestSetSheetProps(t *testing.T) { f := NewFile() assert.NoError(t, f.SetSheetProps("Sheet1", nil)) diff --git a/stream.go b/stream.go index 44d8eb710f..aaa45893f8 100644 --- a/stream.go +++ b/stream.go @@ -369,11 +369,11 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt if err != nil { return err } - sw.rawData.WriteString(``) + _, _ = sw.rawData.WriteString(``) for i, val := range values { if val == nil { continue @@ -643,12 +643,12 @@ type bufferedWriter struct { buf bytes.Buffer } -// Write to the in-memory buffer. The err is always nil. +// Write to the in-memory buffer. The error is always nil. func (bw *bufferedWriter) Write(p []byte) (n int, err error) { return bw.buf.Write(p) } -// WriteString wite to the in-memory buffer. The err is always nil. +// WriteString write to the in-memory buffer. The error is always nil. func (bw *bufferedWriter) WriteString(p string) (n int, err error) { return bw.buf.WriteString(p) } diff --git a/stream_test.go b/stream_test.go index a4a0590aae..4e83626bcf 100644 --- a/stream_test.go +++ b/stream_test.go @@ -235,7 +235,7 @@ func TestStreamSetRowNilValues(t *testing.T) { file := NewFile() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) - streamWriter.SetRow("A1", []interface{}{nil, nil, Cell{Value: "foo"}}) + assert.NoError(t, streamWriter.SetRow("A1", []interface{}{nil, nil, Cell{Value: "foo"}})) streamWriter.Flush() ws, err := file.workSheetReader("Sheet1") assert.NoError(t, err) diff --git a/styles.go b/styles.go index bbb21dcc50..15de5f1ab1 100644 --- a/styles.go +++ b/styles.go @@ -2164,6 +2164,10 @@ func newFontColor(font *Font) *xlsxColor { prepareFontColor() fontColor.RGB = getPaletteColor(font.Color) } + if font.ColorIndexed >= 0 && font.ColorIndexed <= len(IndexedColorMapping)+1 { + prepareFontColor() + fontColor.Indexed = font.ColorIndexed + } if font.ColorTheme != nil { prepareFontColor() fontColor.Theme = font.ColorTheme diff --git a/table.go b/table.go index 112882c235..867af9e24e 100644 --- a/table.go +++ b/table.go @@ -221,7 +221,7 @@ func parseAutoFilterOptions(opts string) (*autoFilterOptions, error) { // // err := f.AutoFilter("Sheet1", "A1", "D4", `{"column":"B","expression":"x != blanks"}`) // -// column defines the filter columns in a auto filter range based on simple +// column defines the filter columns in an auto filter range based on simple // criteria // // It isn't sufficient to just specify the filter condition. You must also diff --git a/xmlDrawing.go b/xmlDrawing.go index b52e44970b..5b4628b5e2 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -141,6 +141,26 @@ const ( ColorMappingTypeUnset int = -1 ) +// IndexedColorMapping is the table of default mappings from indexed color value +// to RGB value. Note that 0-7 are redundant of 8-15 to preserve backwards +// compatibility. A legacy indexing scheme for colors that is still required +// for some records, and for backwards compatibility with legacy formats. This +// element contains a sequence of RGB color values that correspond to color +// indexes (zero-based). When using the default indexed color palette, the +// values are not written out, but instead are implied. When the color palette +// has been modified from default, then the entire color palette is written +// out. +var IndexedColorMapping = []string{ + "000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", + "000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", + "800000", "008000", "000080", "808000", "800080", "008080", "C0C0C0", "808080", + "9999FF", "993366", "FFFFCC", "CCFFFF", "660066", "FF8080", "0066CC", "CCCCFF", + "000080", "FF00FF", "FFFF00", "00FFFF", "800080", "800000", "008080", "0000FF", + "00CCFF", "CCFFFF", "CCFFCC", "FFFF99", "99CCFF", "FF99CC", "CC99FF", "FFCC99", + "3366FF", "33CCCC", "99CC00", "FFCC00", "FF9900", "FF6600", "666699", "969696", + "003366", "339966", "003300", "333300", "993300", "993366", "333399", "333333", +} + // supportedImageTypes defined supported image types. var supportedImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff", ".emf": ".emf", ".wmf": ".wmf", ".emz": ".emz", ".wmz": ".wmz"} diff --git a/xmlStyles.go b/xmlStyles.go index e35dbddc93..c9e0761f73 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -334,16 +334,17 @@ type Border struct { // Font directly maps the font settings of the fonts. type Font struct { - Bold bool `json:"bold"` - Italic bool `json:"italic"` - Underline string `json:"underline"` - Family string `json:"family"` - Size float64 `json:"size"` - Strike bool `json:"strike"` - Color string `json:"color"` - ColorTheme *int `json:"color_theme"` - ColorTint float64 `json:"color_tint"` - VertAlign string `json:"vertAlign"` + Bold bool `json:"bold"` + Italic bool `json:"italic"` + Underline string `json:"underline"` + Family string `json:"family"` + Size float64 `json:"size"` + Strike bool `json:"strike"` + Color string `json:"color"` + ColorIndexed int `json:"color_indexed"` + ColorTheme *int `json:"color_theme"` + ColorTint float64 `json:"color_tint"` + VertAlign string `json:"vertAlign"` } // Fill directly maps the fill settings of the cells. From f44153ea4679247070d6f1e31bb0934a10bebb31 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 25 Oct 2022 10:24:45 +0800 Subject: [PATCH 121/213] This closes #1377, stream writer writes inline string type for string cell value - Add `CellTypeFormula`, `CellTypeInlineString`, `CellTypeSharedString` and remove `CellTypeString` in `CellType` enumeration - Unit tests updated --- calc_test.go | 4 +- cell.go | 151 +++++++++++++++++++++++++++++++++++++++++--------- cell_test.go | 23 ++++---- col_test.go | 6 +- rows.go | 77 ------------------------- rows_test.go | 4 +- sheet_test.go | 6 +- stream.go | 38 +++++++++---- 8 files changed, 172 insertions(+), 137 deletions(-) diff --git a/calc_test.go b/calc_test.go index 1a8b8c62ec..5d61712f9b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -5223,8 +5223,8 @@ func TestCalcXLOOKUP(t *testing.T) { "=XLOOKUP(29,C2:H2,C3:H3,NA(),-1,1)": "D3", } for formula, expected := range formulaList { - assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula)) - result, err := f.CalcCellValue("Sheet1", "D3") + assert.NoError(t, f.SetCellFormula("Sheet1", "D4", formula)) + result, err := f.CalcCellValue("Sheet1", "D4") assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } diff --git a/cell.go b/cell.go index 3fcbb7b3a4..6ed7f48599 100644 --- a/cell.go +++ b/cell.go @@ -30,8 +30,10 @@ const ( CellTypeBool CellTypeDate CellTypeError + CellTypeFormula + CellTypeInlineString CellTypeNumber - CellTypeString + CellTypeSharedString ) const ( @@ -51,9 +53,9 @@ var cellTypes = map[string]CellType{ "d": CellTypeDate, "n": CellTypeNumber, "e": CellTypeError, - "s": CellTypeString, - "str": CellTypeString, - "inlineStr": CellTypeString, + "s": CellTypeSharedString, + "str": CellTypeFormula, + "inlineStr": CellTypeInlineString, } // GetCellValue provides a function to get formatted value from cell by given @@ -235,8 +237,7 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error { date1904 = wb.WorkbookPr.Date1904 } var isNum bool - c.T, c.V, isNum, err = setCellTime(value, date1904) - if err != nil { + if isNum, err = c.setCellTime(value, date1904); err != nil { return err } if isNum { @@ -247,7 +248,7 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error { // setCellTime prepares cell type and Excel time by given Go time.Time type // timestamp. -func setCellTime(value time.Time, date1904 bool) (t string, b string, isNum bool, err error) { +func (c *xlsxC) setCellTime(value time.Time, date1904 bool) (isNum bool, err error) { var excelTime float64 _, offset := value.In(value.Location()).Zone() value = value.Add(time.Duration(offset) * time.Second) @@ -256,9 +257,9 @@ func setCellTime(value time.Time, date1904 bool) (t string, b string, isNum bool } isNum = excelTime > 0 if isNum { - t, b = setCellDefault(strconv.FormatFloat(excelTime, 'f', -1, 64)) + c.setCellDefault(strconv.FormatFloat(excelTime, 'f', -1, 64)) } else { - t, b = setCellDefault(value.Format(time.RFC3339Nano)) + c.setCellDefault(value.Format(time.RFC3339Nano)) } return } @@ -435,14 +436,14 @@ func (f *File) setSharedString(val string) (int, error) { sst.Count++ sst.UniqueCount++ t := xlsxT{Val: val} - _, val, t.Space = setCellStr(val) + val, t.Space = trimCellValue(val) sst.SI = append(sst.SI, xlsxSI{T: &t}) f.sharedStringsMap[val] = sst.UniqueCount - 1 return sst.UniqueCount - 1, nil } -// setCellStr provides a function to set string type to cell. -func setCellStr(value string) (t string, v string, ns xml.Attr) { +// trimCellValue provides a function to set string type to cell. +func trimCellValue(value string) (v string, ns xml.Attr) { if len(value) > TotalCellChars { value = value[:TotalCellChars] } @@ -458,10 +459,117 @@ func setCellStr(value string) (t string, v string, ns xml.Attr) { } } } - t, v = "str", bstrMarshal(value) + v = bstrMarshal(value) return } +// setCellValue set cell data type and value for (inline) rich string cell or +// formula cell. +func (c *xlsxC) setCellValue(val string) { + if c.F != nil { + c.setStr(val) + return + } + c.setInlineStr(val) +} + +// setInlineStr set cell data type and value which containing an (inline) rich +// string. +func (c *xlsxC) setInlineStr(val string) { + c.T, c.V, c.IS = "inlineStr", "", &xlsxSI{T: &xlsxT{}} + c.IS.T.Val, c.IS.T.Space = trimCellValue(val) +} + +// setStr set cell data type and value which containing a formula string. +func (c *xlsxC) setStr(val string) { + c.T, c.IS = "str", nil + c.V, c.XMLSpace = trimCellValue(val) +} + +// getCellDate parse cell value which containing a boolean. +func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) { + if !raw { + if c.V == "1" { + return "TRUE", nil + } + if c.V == "0" { + return "FALSE", nil + } + } + return f.formattedValue(c.S, c.V, raw), nil +} + +// setCellDefault prepares cell type and string type cell value by a given +// string. +func (c *xlsxC) setCellDefault(value string) { + if ok, _, _ := isNumeric(value); !ok { + c.setInlineStr(value) + c.IS.T.Val = value + return + } + c.V = value +} + +// getCellDate parse cell value which contains a date in the ISO 8601 format. +func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) { + if !raw { + layout := "20060102T150405.999" + if strings.HasSuffix(c.V, "Z") { + layout = "20060102T150405Z" + if strings.Contains(c.V, "-") { + layout = "2006-01-02T15:04:05Z" + } + } else if strings.Contains(c.V, "-") { + layout = "2006-01-02 15:04:05Z" + } + if timestamp, err := time.Parse(layout, strings.ReplaceAll(c.V, ",", ".")); err == nil { + excelTime, _ := timeToExcelTime(timestamp, false) + c.V = strconv.FormatFloat(excelTime, 'G', 15, 64) + } + } + return f.formattedValue(c.S, c.V, raw), nil +} + +// getValueFrom return a value from a column/row cell, this function is +// intended to be used with for range on rows an argument with the spreadsheet +// opened file. +func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { + f.Lock() + defer f.Unlock() + switch c.T { + case "b": + return c.getCellBool(f, raw) + case "d": + return c.getCellDate(f, raw) + case "s": + if c.V != "" { + xlsxSI := 0 + xlsxSI, _ = strconv.Atoi(c.V) + if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { + return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw), nil + } + if len(d.SI) > xlsxSI { + return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw), nil + } + } + return f.formattedValue(c.S, c.V, raw), nil + case "inlineStr": + if c.IS != nil { + return f.formattedValue(c.S, c.IS.String(), raw), nil + } + return f.formattedValue(c.S, c.V, raw), nil + default: + if isNum, precision, decimal := isNumeric(c.V); isNum && !raw { + if precision > 15 { + c.V = strconv.FormatFloat(decimal, 'G', 15, 64) + } else { + c.V = strconv.FormatFloat(decimal, 'f', -1, 64) + } + } + return f.formattedValue(c.S, c.V, raw), nil + } +} + // SetCellDefault provides a function to set string type value of a cell as // default format without escaping the cell. func (f *File) SetCellDefault(sheet, cell, value string) error { @@ -476,22 +584,11 @@ func (f *File) SetCellDefault(sheet, cell, value string) error { ws.Lock() defer ws.Unlock() c.S = f.prepareCellStyle(ws, col, row, c.S) - c.T, c.V = setCellDefault(value) - c.IS = nil + c.setCellDefault(value) f.removeFormula(c, ws, sheet) return err } -// setCellDefault prepares cell type and string type cell value by a given -// string. -func setCellDefault(value string) (t string, v string) { - if ok, _, _ := isNumeric(value); !ok { - t = "str" - } - v = value - return -} - // GetCellFormula provides a function to get formula from cell by given // worksheet name and cell reference in spreadsheet. func (f *File) GetCellFormula(sheet, cell string) (string, error) { @@ -625,7 +722,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) c.F.Ref = *opt.Ref } } - c.IS = nil + c.T, c.IS = "str", nil return err } @@ -900,7 +997,7 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { return textRuns, ErrCellCharsLength } run := xlsxR{T: &xlsxT{}} - _, run.T.Val, run.T.Space = setCellStr(textRun.Text) + run.T.Val, run.T.Space = trimCellValue(textRun.Text) fnt := textRun.Font if fnt != nil { run.RPr = newRpr(fnt) diff --git a/cell_test.go b/cell_test.go index 980058a30e..f7412111d4 100644 --- a/cell_test.go +++ b/cell_test.go @@ -224,10 +224,11 @@ func TestSetCellTime(t *testing.T) { } { timezone, err := time.LoadLocation(location) assert.NoError(t, err) - _, b, isNum, err := setCellTime(date.In(timezone), false) + c := &xlsxC{} + isNum, err := c.setCellTime(date.In(timezone), false) assert.NoError(t, err) assert.Equal(t, true, isNum) - assert.Equal(t, expected, b) + assert.Equal(t, expected, c.V) } } @@ -237,7 +238,7 @@ func TestGetCellValue(t *testing.T) { sheetData := `%s` f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) f.checked = nil cells := []string{"A3", "A4", "B4", "A7", "B7"} rows, err := f.GetRows("Sheet1") @@ -253,35 +254,35 @@ 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, `A2B2`))) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2B2`))) f.checked = nil cell, err := f.GetCellValue("Sheet1", "A2") assert.Equal(t, "A2", cell) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2B2`))) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2B2`))) f.checked = nil rows, err = f.GetRows("Sheet1") assert.Equal(t, [][]string{nil, {"A2", "B2"}}, rows) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A1B1`))) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A1B1`))) f.checked = nil rows, err = f.GetRows("Sheet1") assert.Equal(t, [][]string{{"A1", "B1"}}, rows) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) f.checked = nil rows, err = f.GetRows("Sheet1") assert.Equal(t, [][]string{{"A3"}, {"A4", "B4"}, nil, nil, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `H6r0A6F4A6B6C6100B3`))) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `H6r0A6F4A6B6C6100B3`))) f.checked = nil cell, err = f.GetCellValue("Sheet1", "H6") assert.Equal(t, "H6", cell) @@ -326,8 +327,8 @@ func TestGetCellValue(t *testing.T) { 275.39999999999998 68.900000000000006 1.1000000000000001 - 1234567890123_4 - 123456789_0123_4 + 1234567890123_4 + 123456789_0123_4 +0.0000000000000000002399999999999992E-4 7.2399999999999992E-2 20200208T080910.123 @@ -386,7 +387,7 @@ func TestGetCellType(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", "A1", "A1")) cellType, err = f.GetCellType("Sheet1", "A1") assert.NoError(t, err) - assert.Equal(t, CellTypeString, cellType) + assert.Equal(t, CellTypeSharedString, cellType) _, err = f.GetCellType("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } diff --git a/col_test.go b/col_test.go index 75c191b93a..f786335709 100644 --- a/col_test.go +++ b/col_test.go @@ -109,12 +109,12 @@ func TestGetColsError(t *testing.T) { f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) f.checked = nil _, err = f.GetCols("Sheet1") assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) _, err = f.GetCols("Sheet1") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) @@ -124,7 +124,7 @@ func TestGetColsError(t *testing.T) { cols.totalRows = 2 cols.totalCols = 2 cols.curCol = 1 - cols.sheetXML = []byte(`A`) + cols.sheetXML = []byte(`A`) _, err = cols.Rows() assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) diff --git a/rows.go b/rows.go index 9f791cb80b..4f05f24314 100644 --- a/rows.go +++ b/rows.go @@ -20,8 +20,6 @@ import ( "math" "os" "strconv" - "strings" - "time" "github.com/mohae/deepcopy" ) @@ -449,81 +447,6 @@ func (f *File) sharedStringsReader() *xlsxSST { return f.SharedStrings } -// getCellDate parse cell value which containing a boolean. -func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) { - if !raw { - if c.V == "1" { - return "TRUE", nil - } - if c.V == "0" { - return "FALSE", nil - } - } - return f.formattedValue(c.S, c.V, raw), nil -} - -// getCellDate parse cell value which contains a date in the ISO 8601 format. -func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) { - if !raw { - layout := "20060102T150405.999" - if strings.HasSuffix(c.V, "Z") { - layout = "20060102T150405Z" - if strings.Contains(c.V, "-") { - layout = "2006-01-02T15:04:05Z" - } - } else if strings.Contains(c.V, "-") { - layout = "2006-01-02 15:04:05Z" - } - if timestamp, err := time.Parse(layout, strings.ReplaceAll(c.V, ",", ".")); err == nil { - excelTime, _ := timeToExcelTime(timestamp, false) - c.V = strconv.FormatFloat(excelTime, 'G', 15, 64) - } - } - return f.formattedValue(c.S, c.V, raw), nil -} - -// getValueFrom return a value from a column/row cell, this function is -// intended to be used with for range on rows an argument with the spreadsheet -// opened file. -func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { - f.Lock() - defer f.Unlock() - switch c.T { - case "b": - return c.getCellBool(f, raw) - case "d": - return c.getCellDate(f, raw) - case "s": - if c.V != "" { - xlsxSI := 0 - xlsxSI, _ = strconv.Atoi(c.V) - if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { - return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw), nil - } - if len(d.SI) > xlsxSI { - return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw), nil - } - } - return f.formattedValue(c.S, c.V, raw), nil - case "str": - return f.formattedValue(c.S, c.V, raw), nil - case "inlineStr": - if c.IS != nil { - return f.formattedValue(c.S, c.IS.String(), raw), nil - } - return f.formattedValue(c.S, c.V, raw), nil - default: - if isNum, precision, decimal := isNumeric(c.V); isNum && !raw { - if precision > 15 { - c.V = strconv.FormatFloat(decimal, 'G', 15, 64) - } else { - c.V = strconv.FormatFloat(decimal, 'f', -1, 64) - } - } - return f.formattedValue(c.S, c.V, raw), nil - } -} - // SetRowVisible provides a function to set visible of a single row by given // worksheet name and Excel row number. For example, hide row 2 in Sheet1: // diff --git a/rows_test.go b/rows_test.go index 423932f8ac..81572e1852 100644 --- a/rows_test.go +++ b/rows_test.go @@ -203,12 +203,12 @@ func TestColumns(t *testing.T) { _, err = rows.Columns() assert.NoError(t, err) - rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) assert.True(t, rows.Next()) _, err = rows.Columns() assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) - rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) _, err = rows.Columns() assert.NoError(t, err) diff --git a/sheet_test.go b/sheet_test.go index 6e87de9cb0..4e1e44818b 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -76,18 +76,18 @@ func TestSearchSheet(t *testing.T) { f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) f.checked = nil result, err = f.SearchSheet("Sheet1", "A") assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax") assert.Equal(t, []string(nil), result) - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) result, err = f.SearchSheet("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, []string(nil), result) - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) result, err = f.SearchSheet("Sheet1", "A") assert.EqualError(t, err, "invalid cell reference [1, 0]") assert.Equal(t, []string(nil), result) diff --git a/stream.go b/stream.go index aaa45893f8..fa78d8bb91 100644 --- a/stream.go +++ b/stream.go @@ -263,7 +263,7 @@ func (sw *StreamWriter) getRowValues(hRow, hCol, vCol int) (res []string, err er if col < hCol || col > vCol { continue } - res[col-hCol] = c.V + res[col-hCol], _ = c.getValueFrom(sw.File, nil, false) } return res, nil } @@ -462,7 +462,7 @@ func (sw *StreamWriter) MergeCell(hCell, vCell string) error { // setCellFormula provides a function to set formula of a cell. func setCellFormula(c *xlsxC, formula string) { if formula != "" { - c.F = &xlsxF{Content: formula} + c.T, c.F = "str", &xlsxF{Content: formula} } } @@ -477,9 +477,9 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error { case float64: c.T, c.V = setCellFloat(val, -1, 64) case string: - c.T, c.V, c.XMLSpace = setCellStr(val) + c.setCellValue(val) case []byte: - c.T, c.V, c.XMLSpace = setCellStr(string(val)) + c.setCellValue(string(val)) case time.Duration: c.T, c.V = setCellDuration(val) case time.Time: @@ -488,20 +488,19 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error { if wb != nil && wb.WorkbookPr != nil { date1904 = wb.WorkbookPr.Date1904 } - c.T, c.V, isNum, err = setCellTime(val, date1904) - if isNum && c.S == 0 { + if isNum, err = c.setCellTime(val, date1904); isNum && c.S == 0 { style, _ := sw.File.NewStyle(&Style{NumFmt: 22}) c.S = style } case bool: c.T, c.V = setCellBool(val) case nil: - c.T, c.V, c.XMLSpace = setCellStr("") + c.setCellValue("") case []RichTextRun: c.T, c.IS = "inlineStr", &xlsxSI{} c.IS.R, err = setRichText(val) default: - c.T, c.V, c.XMLSpace = setCellStr(fmt.Sprint(val)) + c.setCellValue(fmt.Sprint(val)) } return err } @@ -569,10 +568,25 @@ func writeCell(buf *bufferedWriter, c xlsxC) { _, _ = buf.WriteString(``) } if c.IS != nil { - is, _ := xml.Marshal(c.IS.R) - _, _ = buf.WriteString(``) - _, _ = buf.Write(is) - _, _ = buf.WriteString(``) + if len(c.IS.R) > 0 { + is, _ := xml.Marshal(c.IS.R) + _, _ = buf.WriteString(``) + _, _ = buf.Write(is) + _, _ = buf.WriteString(``) + } + if c.IS.T != nil { + _, _ = buf.WriteString(``) + _, _ = buf.Write([]byte(c.IS.T.Val)) + _, _ = buf.WriteString(``) + } } _, _ = buf.WriteString(``) } From adf9d37d82edd3dbc365fece76a031a92e2220d6 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 26 Oct 2022 00:04:23 +0800 Subject: [PATCH 122/213] This closes #1379, cleanup stream writer temporary files by the `Close` function - Fix error on inserting columns or rows on the worksheet which contains one cell merged cell range - Fix getting incomplete rich text cell value in some cases - Unit tests updated --- adjust.go | 6 +++++- adjust_test.go | 9 +++++++++ cell.go | 20 ++++++++++---------- file.go | 4 +++- stream.go | 5 +++++ stream_test.go | 35 ++++++++++++++++++++++++++++++++++- 6 files changed, 66 insertions(+), 13 deletions(-) diff --git a/adjust.go b/adjust.go index 92efcd0af3..65e82fca03 100644 --- a/adjust.go +++ b/adjust.go @@ -278,7 +278,11 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off for i := 0; i < len(ws.MergeCells.Cells); i++ { mergedCells := ws.MergeCells.Cells[i] - coordinates, err := rangeRefToCoordinates(mergedCells.Ref) + mergedCellsRef := mergedCells.Ref + if !strings.Contains(mergedCellsRef, ":") { + mergedCellsRef += ":" + mergedCellsRef + } + coordinates, err := rangeRefToCoordinates(mergedCellsRef) if err != nil { return err } diff --git a/adjust_test.go b/adjust_test.go index a3e73abea8..010955c60f 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -47,6 +47,15 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, columns, 1, -1)) + assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A2", + }, + }, + }, + }, columns, 1, -1)) // testing adjustMergeCells var cases []struct { diff --git a/cell.go b/cell.go index 6ed7f48599..fbc84b7d05 100644 --- a/cell.go +++ b/cell.go @@ -152,19 +152,19 @@ func (f *File) SetCellValue(sheet, cell string, value interface{}) error { // String extracts characters from a string item. func (x xlsxSI) String() string { - if len(x.R) > 0 { - var rows strings.Builder - for _, s := range x.R { - if s.T != nil { - rows.WriteString(s.T.Val) - } + var value strings.Builder + if x.T != nil { + value.WriteString(x.T.Val) + } + for _, s := range x.R { + if s.T != nil { + value.WriteString(s.T.Val) } - return bstrUnmarshal(rows.String()) } - if x.T != nil { - return bstrUnmarshal(x.T.Val) + if value.Len() == 0 { + return "" } - return "" + return bstrUnmarshal(value.String()) } // hasValue determine if cell non-blank value. diff --git a/file.go b/file.go index 43a37dd294..1469af0977 100644 --- a/file.go +++ b/file.go @@ -97,6 +97,9 @@ func (f *File) Close() error { } return true }) + for _, stream := range f.streams { + _ = stream.rawData.Close() + } return err } @@ -195,7 +198,6 @@ func (f *File) writeToZip(zw *zip.Writer) error { if err != nil { return err } - _ = stream.rawData.Close() } var err error f.Pkg.Range(func(path, content interface{}) bool { diff --git a/stream.go b/stream.go index fa78d8bb91..766e83a47c 100644 --- a/stream.go +++ b/stream.go @@ -48,6 +48,11 @@ type StreamWriter struct { // with numbers and style: // // file := excelize.NewFile() +// defer func() { +// if err := file.Close(); err != nil { +// fmt.Println(err) +// } +// }() // streamWriter, err := file.NewStreamWriter("Sheet1") // if err != nil { // fmt.Println(err) diff --git a/stream_test.go b/stream_test.go index 4e83626bcf..040eee0783 100644 --- a/stream_test.go +++ b/stream_test.go @@ -15,7 +15,11 @@ import ( func BenchmarkStreamWriter(b *testing.B) { file := NewFile() - + defer func() { + if err := file.Close(); err != nil { + b.Error(err) + } + }() row := make([]interface{}, 10) for colID := 0; colID < 10; colID++ { row[colID] = colID @@ -78,6 +82,7 @@ func TestStreamWriter(t *testing.T) { // Test set cell column overflow. assert.ErrorIs(t, streamWriter.SetRow("XFD51201", []interface{}{"A", "B", "C"}), ErrColumnNumber) + assert.NoError(t, file.Close()) // Test close temporary file error. file = NewFile() @@ -107,6 +112,7 @@ func TestStreamWriter(t *testing.T) { file.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) _, err = file.NewStreamWriter("Sheet1") assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, file.Close()) // Test read cell. file = NewFile() @@ -138,6 +144,9 @@ func TestStreamWriter(t *testing.T) { func TestStreamSetColWidth(t *testing.T) { file := NewFile() + defer func() { + assert.NoError(t, file.Close()) + }() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.SetColWidth(3, 2, 20)) @@ -150,6 +159,9 @@ func TestStreamSetColWidth(t *testing.T) { func TestStreamSetPanes(t *testing.T) { file, paneOpts := NewFile(), `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}` + defer func() { + assert.NoError(t, file.Close()) + }() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.SetPanes(paneOpts)) @@ -160,6 +172,9 @@ func TestStreamSetPanes(t *testing.T) { func TestStreamTable(t *testing.T) { file := NewFile() + defer func() { + assert.NoError(t, file.Close()) + }() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) @@ -194,6 +209,9 @@ func TestStreamTable(t *testing.T) { func TestStreamMergeCells(t *testing.T) { file := NewFile() + defer func() { + assert.NoError(t, file.Close()) + }() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.MergeCell("A1", "D1")) @@ -207,6 +225,9 @@ func TestStreamMergeCells(t *testing.T) { func TestNewStreamWriter(t *testing.T) { // Test error exceptions file := NewFile() + defer func() { + assert.NoError(t, file.Close()) + }() _, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) _, err = file.NewStreamWriter("SheetN") @@ -223,6 +244,9 @@ func TestStreamMarshalAttrs(t *testing.T) { func TestStreamSetRow(t *testing.T) { // Test error exceptions file := NewFile() + defer func() { + assert.NoError(t, file.Close()) + }() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) @@ -233,6 +257,9 @@ func TestStreamSetRow(t *testing.T) { func TestStreamSetRowNilValues(t *testing.T) { file := NewFile() + defer func() { + assert.NoError(t, file.Close()) + }() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{nil, nil, Cell{Value: "foo"}})) @@ -244,6 +271,9 @@ func TestStreamSetRowNilValues(t *testing.T) { func TestStreamSetRowWithStyle(t *testing.T) { file := NewFile() + defer func() { + assert.NoError(t, file.Close()) + }() zeroStyleID := 0 grayStyleID, err := file.NewStyle(&Style{Font: &Font{Color: "#777777"}}) assert.NoError(t, err) @@ -273,6 +303,9 @@ func TestStreamSetRowWithStyle(t *testing.T) { func TestStreamSetCellValFunc(t *testing.T) { f := NewFile() + defer func() { + assert.NoError(t, f.Close()) + }() sw, err := f.NewStreamWriter("Sheet1") assert.NoError(t, err) c := &xlsxC{} From a410b22bdd50e9f212b0b454e5aed798e3476394 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 28 Oct 2022 00:31:55 +0800 Subject: [PATCH 123/213] Fix the error on getting the range of merged cells on the worksheet which contains one cell merged cell range - Parse workbook default theme for custom theme color support in the feature - Variables name typo fix - Add system foreground and background color as RGB in the IndexedColorMapping list --- calc.go | 388 ++++++++++++++++++++++++------------------------- drawing.go | 4 +- errors.go | 6 + file.go | 1 + lib.go | 8 +- merge.go | 12 +- merge_test.go | 6 +- rows.go | 12 +- shape.go | 2 +- sheet.go | 10 +- sheetview.go | 6 +- styles.go | 19 ++- styles_test.go | 4 +- templates.go | 1 + xmlChart.go | 57 ++++---- xmlDrawing.go | 1 + xmlTheme.go | 142 +++++++++--------- 17 files changed, 357 insertions(+), 322 deletions(-) diff --git a/calc.go b/calc.go index c600aaa32b..b4090c9de4 100644 --- a/calc.go +++ b/calc.go @@ -59,19 +59,19 @@ const ( criteriaErr criteriaRegexp - catgoryWeightAndMass - catgoryDistance - catgoryTime - catgoryPressure - catgoryForce - catgoryEnergy - catgoryPower - catgoryMagnetism - catgoryTemperature - catgoryVolumeAndLiquidMeasure - catgoryArea - catgoryInformation - catgorySpeed + categoryWeightAndMass + categoryDistance + categoryTime + categoryPressure + categoryForce + categoryEnergy + categoryPower + categoryMagnetism + categoryTemperature + categoryVolumeAndLiquidMeasure + categoryArea + categoryInformation + categorySpeed matchModeExact = 0 matchModeMinGreater = 1 @@ -2144,177 +2144,177 @@ type conversionUnit struct { // formula function CONVERT. var conversionUnits = map[string]conversionUnit{ // weight and mass - "g": {group: catgoryWeightAndMass, allowPrefix: true}, - "sg": {group: catgoryWeightAndMass, allowPrefix: false}, - "lbm": {group: catgoryWeightAndMass, allowPrefix: false}, - "u": {group: catgoryWeightAndMass, allowPrefix: true}, - "ozm": {group: catgoryWeightAndMass, allowPrefix: false}, - "grain": {group: catgoryWeightAndMass, allowPrefix: false}, - "cwt": {group: catgoryWeightAndMass, allowPrefix: false}, - "shweight": {group: catgoryWeightAndMass, allowPrefix: false}, - "uk_cwt": {group: catgoryWeightAndMass, allowPrefix: false}, - "lcwt": {group: catgoryWeightAndMass, allowPrefix: false}, - "hweight": {group: catgoryWeightAndMass, allowPrefix: false}, - "stone": {group: catgoryWeightAndMass, allowPrefix: false}, - "ton": {group: catgoryWeightAndMass, allowPrefix: false}, - "uk_ton": {group: catgoryWeightAndMass, allowPrefix: false}, - "LTON": {group: catgoryWeightAndMass, allowPrefix: false}, - "brton": {group: catgoryWeightAndMass, allowPrefix: false}, + "g": {group: categoryWeightAndMass, allowPrefix: true}, + "sg": {group: categoryWeightAndMass, allowPrefix: false}, + "lbm": {group: categoryWeightAndMass, allowPrefix: false}, + "u": {group: categoryWeightAndMass, allowPrefix: true}, + "ozm": {group: categoryWeightAndMass, allowPrefix: false}, + "grain": {group: categoryWeightAndMass, allowPrefix: false}, + "cwt": {group: categoryWeightAndMass, allowPrefix: false}, + "shweight": {group: categoryWeightAndMass, allowPrefix: false}, + "uk_cwt": {group: categoryWeightAndMass, allowPrefix: false}, + "lcwt": {group: categoryWeightAndMass, allowPrefix: false}, + "hweight": {group: categoryWeightAndMass, allowPrefix: false}, + "stone": {group: categoryWeightAndMass, allowPrefix: false}, + "ton": {group: categoryWeightAndMass, allowPrefix: false}, + "uk_ton": {group: categoryWeightAndMass, allowPrefix: false}, + "LTON": {group: categoryWeightAndMass, allowPrefix: false}, + "brton": {group: categoryWeightAndMass, allowPrefix: false}, // distance - "m": {group: catgoryDistance, allowPrefix: true}, - "mi": {group: catgoryDistance, allowPrefix: false}, - "Nmi": {group: catgoryDistance, allowPrefix: false}, - "in": {group: catgoryDistance, allowPrefix: false}, - "ft": {group: catgoryDistance, allowPrefix: false}, - "yd": {group: catgoryDistance, allowPrefix: false}, - "ang": {group: catgoryDistance, allowPrefix: true}, - "ell": {group: catgoryDistance, allowPrefix: false}, - "ly": {group: catgoryDistance, allowPrefix: false}, - "parsec": {group: catgoryDistance, allowPrefix: false}, - "pc": {group: catgoryDistance, allowPrefix: false}, - "Pica": {group: catgoryDistance, allowPrefix: false}, - "Picapt": {group: catgoryDistance, allowPrefix: false}, - "pica": {group: catgoryDistance, allowPrefix: false}, - "survey_mi": {group: catgoryDistance, allowPrefix: false}, + "m": {group: categoryDistance, allowPrefix: true}, + "mi": {group: categoryDistance, allowPrefix: false}, + "Nmi": {group: categoryDistance, allowPrefix: false}, + "in": {group: categoryDistance, allowPrefix: false}, + "ft": {group: categoryDistance, allowPrefix: false}, + "yd": {group: categoryDistance, allowPrefix: false}, + "ang": {group: categoryDistance, allowPrefix: true}, + "ell": {group: categoryDistance, allowPrefix: false}, + "ly": {group: categoryDistance, allowPrefix: false}, + "parsec": {group: categoryDistance, allowPrefix: false}, + "pc": {group: categoryDistance, allowPrefix: false}, + "Pica": {group: categoryDistance, allowPrefix: false}, + "Picapt": {group: categoryDistance, allowPrefix: false}, + "pica": {group: categoryDistance, allowPrefix: false}, + "survey_mi": {group: categoryDistance, allowPrefix: false}, // time - "yr": {group: catgoryTime, allowPrefix: false}, - "day": {group: catgoryTime, allowPrefix: false}, - "d": {group: catgoryTime, allowPrefix: false}, - "hr": {group: catgoryTime, allowPrefix: false}, - "mn": {group: catgoryTime, allowPrefix: false}, - "min": {group: catgoryTime, allowPrefix: false}, - "sec": {group: catgoryTime, allowPrefix: true}, - "s": {group: catgoryTime, allowPrefix: true}, + "yr": {group: categoryTime, allowPrefix: false}, + "day": {group: categoryTime, allowPrefix: false}, + "d": {group: categoryTime, allowPrefix: false}, + "hr": {group: categoryTime, allowPrefix: false}, + "mn": {group: categoryTime, allowPrefix: false}, + "min": {group: categoryTime, allowPrefix: false}, + "sec": {group: categoryTime, allowPrefix: true}, + "s": {group: categoryTime, allowPrefix: true}, // pressure - "Pa": {group: catgoryPressure, allowPrefix: true}, - "p": {group: catgoryPressure, allowPrefix: true}, - "atm": {group: catgoryPressure, allowPrefix: true}, - "at": {group: catgoryPressure, allowPrefix: true}, - "mmHg": {group: catgoryPressure, allowPrefix: true}, - "psi": {group: catgoryPressure, allowPrefix: true}, - "Torr": {group: catgoryPressure, allowPrefix: true}, + "Pa": {group: categoryPressure, allowPrefix: true}, + "p": {group: categoryPressure, allowPrefix: true}, + "atm": {group: categoryPressure, allowPrefix: true}, + "at": {group: categoryPressure, allowPrefix: true}, + "mmHg": {group: categoryPressure, allowPrefix: true}, + "psi": {group: categoryPressure, allowPrefix: true}, + "Torr": {group: categoryPressure, allowPrefix: true}, // force - "N": {group: catgoryForce, allowPrefix: true}, - "dyn": {group: catgoryForce, allowPrefix: true}, - "dy": {group: catgoryForce, allowPrefix: true}, - "lbf": {group: catgoryForce, allowPrefix: false}, - "pond": {group: catgoryForce, allowPrefix: true}, + "N": {group: categoryForce, allowPrefix: true}, + "dyn": {group: categoryForce, allowPrefix: true}, + "dy": {group: categoryForce, allowPrefix: true}, + "lbf": {group: categoryForce, allowPrefix: false}, + "pond": {group: categoryForce, allowPrefix: true}, // energy - "J": {group: catgoryEnergy, allowPrefix: true}, - "e": {group: catgoryEnergy, allowPrefix: true}, - "c": {group: catgoryEnergy, allowPrefix: true}, - "cal": {group: catgoryEnergy, allowPrefix: true}, - "eV": {group: catgoryEnergy, allowPrefix: true}, - "ev": {group: catgoryEnergy, allowPrefix: true}, - "HPh": {group: catgoryEnergy, allowPrefix: false}, - "hh": {group: catgoryEnergy, allowPrefix: false}, - "Wh": {group: catgoryEnergy, allowPrefix: true}, - "wh": {group: catgoryEnergy, allowPrefix: true}, - "flb": {group: catgoryEnergy, allowPrefix: false}, - "BTU": {group: catgoryEnergy, allowPrefix: false}, - "btu": {group: catgoryEnergy, allowPrefix: false}, + "J": {group: categoryEnergy, allowPrefix: true}, + "e": {group: categoryEnergy, allowPrefix: true}, + "c": {group: categoryEnergy, allowPrefix: true}, + "cal": {group: categoryEnergy, allowPrefix: true}, + "eV": {group: categoryEnergy, allowPrefix: true}, + "ev": {group: categoryEnergy, allowPrefix: true}, + "HPh": {group: categoryEnergy, allowPrefix: false}, + "hh": {group: categoryEnergy, allowPrefix: false}, + "Wh": {group: categoryEnergy, allowPrefix: true}, + "wh": {group: categoryEnergy, allowPrefix: true}, + "flb": {group: categoryEnergy, allowPrefix: false}, + "BTU": {group: categoryEnergy, allowPrefix: false}, + "btu": {group: categoryEnergy, allowPrefix: false}, // power - "HP": {group: catgoryPower, allowPrefix: false}, - "h": {group: catgoryPower, allowPrefix: false}, - "W": {group: catgoryPower, allowPrefix: true}, - "w": {group: catgoryPower, allowPrefix: true}, - "PS": {group: catgoryPower, allowPrefix: false}, - "T": {group: catgoryMagnetism, allowPrefix: true}, - "ga": {group: catgoryMagnetism, allowPrefix: true}, + "HP": {group: categoryPower, allowPrefix: false}, + "h": {group: categoryPower, allowPrefix: false}, + "W": {group: categoryPower, allowPrefix: true}, + "w": {group: categoryPower, allowPrefix: true}, + "PS": {group: categoryPower, allowPrefix: false}, + "T": {group: categoryMagnetism, allowPrefix: true}, + "ga": {group: categoryMagnetism, allowPrefix: true}, // temperature - "C": {group: catgoryTemperature, allowPrefix: false}, - "cel": {group: catgoryTemperature, allowPrefix: false}, - "F": {group: catgoryTemperature, allowPrefix: false}, - "fah": {group: catgoryTemperature, allowPrefix: false}, - "K": {group: catgoryTemperature, allowPrefix: false}, - "kel": {group: catgoryTemperature, allowPrefix: false}, - "Rank": {group: catgoryTemperature, allowPrefix: false}, - "Reau": {group: catgoryTemperature, allowPrefix: false}, + "C": {group: categoryTemperature, allowPrefix: false}, + "cel": {group: categoryTemperature, allowPrefix: false}, + "F": {group: categoryTemperature, allowPrefix: false}, + "fah": {group: categoryTemperature, allowPrefix: false}, + "K": {group: categoryTemperature, allowPrefix: false}, + "kel": {group: categoryTemperature, allowPrefix: false}, + "Rank": {group: categoryTemperature, allowPrefix: false}, + "Reau": {group: categoryTemperature, allowPrefix: false}, // volume - "l": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, - "L": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, - "lt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, - "tsp": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "tspm": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "tbs": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "oz": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "cup": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "pt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "us_pt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "uk_pt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "qt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "uk_qt": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "gal": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "uk_gal": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "ang3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, - "ang^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, - "barrel": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "bushel": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "in3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "in^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "ft3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "ft^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "ly3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "ly^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "m3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, - "m^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: true}, - "mi3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "mi^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "yd3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "yd^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "Nmi3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "Nmi^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "Pica3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "Pica^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "Picapt3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "Picapt^3": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "GRT": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "regton": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, - "MTON": {group: catgoryVolumeAndLiquidMeasure, allowPrefix: false}, + "l": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true}, + "L": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true}, + "lt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true}, + "tsp": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "tspm": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "tbs": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "oz": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "cup": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "pt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "us_pt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "uk_pt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "qt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "uk_qt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "gal": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "uk_gal": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "ang3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true}, + "ang^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true}, + "barrel": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "bushel": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "in3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "in^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "ft3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "ft^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "ly3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "ly^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "m3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true}, + "m^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true}, + "mi3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "mi^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "yd3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "yd^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Nmi3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Nmi^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Pica3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Pica^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Picapt3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "Picapt^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "GRT": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "regton": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, + "MTON": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false}, // area - "ha": {group: catgoryArea, allowPrefix: true}, - "uk_acre": {group: catgoryArea, allowPrefix: false}, - "us_acre": {group: catgoryArea, allowPrefix: false}, - "ang2": {group: catgoryArea, allowPrefix: true}, - "ang^2": {group: catgoryArea, allowPrefix: true}, - "ar": {group: catgoryArea, allowPrefix: true}, - "ft2": {group: catgoryArea, allowPrefix: false}, - "ft^2": {group: catgoryArea, allowPrefix: false}, - "in2": {group: catgoryArea, allowPrefix: false}, - "in^2": {group: catgoryArea, allowPrefix: false}, - "ly2": {group: catgoryArea, allowPrefix: false}, - "ly^2": {group: catgoryArea, allowPrefix: false}, - "m2": {group: catgoryArea, allowPrefix: true}, - "m^2": {group: catgoryArea, allowPrefix: true}, - "Morgen": {group: catgoryArea, allowPrefix: false}, - "mi2": {group: catgoryArea, allowPrefix: false}, - "mi^2": {group: catgoryArea, allowPrefix: false}, - "Nmi2": {group: catgoryArea, allowPrefix: false}, - "Nmi^2": {group: catgoryArea, allowPrefix: false}, - "Pica2": {group: catgoryArea, allowPrefix: false}, - "Pica^2": {group: catgoryArea, allowPrefix: false}, - "Picapt2": {group: catgoryArea, allowPrefix: false}, - "Picapt^2": {group: catgoryArea, allowPrefix: false}, - "yd2": {group: catgoryArea, allowPrefix: false}, - "yd^2": {group: catgoryArea, allowPrefix: false}, + "ha": {group: categoryArea, allowPrefix: true}, + "uk_acre": {group: categoryArea, allowPrefix: false}, + "us_acre": {group: categoryArea, allowPrefix: false}, + "ang2": {group: categoryArea, allowPrefix: true}, + "ang^2": {group: categoryArea, allowPrefix: true}, + "ar": {group: categoryArea, allowPrefix: true}, + "ft2": {group: categoryArea, allowPrefix: false}, + "ft^2": {group: categoryArea, allowPrefix: false}, + "in2": {group: categoryArea, allowPrefix: false}, + "in^2": {group: categoryArea, allowPrefix: false}, + "ly2": {group: categoryArea, allowPrefix: false}, + "ly^2": {group: categoryArea, allowPrefix: false}, + "m2": {group: categoryArea, allowPrefix: true}, + "m^2": {group: categoryArea, allowPrefix: true}, + "Morgen": {group: categoryArea, allowPrefix: false}, + "mi2": {group: categoryArea, allowPrefix: false}, + "mi^2": {group: categoryArea, allowPrefix: false}, + "Nmi2": {group: categoryArea, allowPrefix: false}, + "Nmi^2": {group: categoryArea, allowPrefix: false}, + "Pica2": {group: categoryArea, allowPrefix: false}, + "Pica^2": {group: categoryArea, allowPrefix: false}, + "Picapt2": {group: categoryArea, allowPrefix: false}, + "Picapt^2": {group: categoryArea, allowPrefix: false}, + "yd2": {group: categoryArea, allowPrefix: false}, + "yd^2": {group: categoryArea, allowPrefix: false}, // information - "byte": {group: catgoryInformation, allowPrefix: true}, - "bit": {group: catgoryInformation, allowPrefix: true}, + "byte": {group: categoryInformation, allowPrefix: true}, + "bit": {group: categoryInformation, allowPrefix: true}, // speed - "m/s": {group: catgorySpeed, allowPrefix: true}, - "m/sec": {group: catgorySpeed, allowPrefix: true}, - "m/h": {group: catgorySpeed, allowPrefix: true}, - "m/hr": {group: catgorySpeed, allowPrefix: true}, - "mph": {group: catgorySpeed, allowPrefix: false}, - "admkn": {group: catgorySpeed, allowPrefix: false}, - "kn": {group: catgorySpeed, allowPrefix: false}, + "m/s": {group: categorySpeed, allowPrefix: true}, + "m/sec": {group: categorySpeed, allowPrefix: true}, + "m/h": {group: categorySpeed, allowPrefix: true}, + "m/hr": {group: categorySpeed, allowPrefix: true}, + "mph": {group: categorySpeed, allowPrefix: false}, + "admkn": {group: categorySpeed, allowPrefix: false}, + "kn": {group: categorySpeed, allowPrefix: false}, } // unitConversions maps details of the Units of measure conversion factors, // organised by group. var unitConversions = map[byte]map[string]float64{ // conversion uses gram (g) as an intermediate unit - catgoryWeightAndMass: { + categoryWeightAndMass: { "g": 1, "sg": 6.85217658567918e-05, "lbm": 2.20462262184878e-03, @@ -2333,7 +2333,7 @@ var unitConversions = map[byte]map[string]float64{ "brton": 9.84206527611061e-07, }, // conversion uses meter (m) as an intermediate unit - catgoryDistance: { + categoryDistance: { "m": 1, "mi": 6.21371192237334e-04, "Nmi": 5.39956803455724e-04, @@ -2351,7 +2351,7 @@ var unitConversions = map[byte]map[string]float64{ "survey_mi": 6.21369949494950e-04, }, // conversion uses second (s) as an intermediate unit - catgoryTime: { + categoryTime: { "yr": 3.16880878140289e-08, "day": 1.15740740740741e-05, "d": 1.15740740740741e-05, @@ -2362,7 +2362,7 @@ var unitConversions = map[byte]map[string]float64{ "s": 1, }, // conversion uses Pascal (Pa) as an intermediate unit - catgoryPressure: { + categoryPressure: { "Pa": 1, "p": 1, "atm": 9.86923266716013e-06, @@ -2372,7 +2372,7 @@ var unitConversions = map[byte]map[string]float64{ "Torr": 7.50061682704170e-03, }, // conversion uses Newton (N) as an intermediate unit - catgoryForce: { + categoryForce: { "N": 1, "dyn": 1.0e+5, "dy": 1.0e+5, @@ -2380,7 +2380,7 @@ var unitConversions = map[byte]map[string]float64{ "pond": 1.01971621297793e+02, }, // conversion uses Joule (J) as an intermediate unit - catgoryEnergy: { + categoryEnergy: { "J": 1, "e": 9.99999519343231e+06, "c": 2.39006249473467e-01, @@ -2396,7 +2396,7 @@ var unitConversions = map[byte]map[string]float64{ "btu": 9.47815067349015e-04, }, // conversion uses Horsepower (HP) as an intermediate unit - catgoryPower: { + categoryPower: { "HP": 1, "h": 1, "W": 7.45699871582270e+02, @@ -2404,12 +2404,12 @@ var unitConversions = map[byte]map[string]float64{ "PS": 1.01386966542400e+00, }, // conversion uses Tesla (T) as an intermediate unit - catgoryMagnetism: { + categoryMagnetism: { "T": 1, "ga": 10000, }, // conversion uses litre (l) as an intermediate unit - catgoryVolumeAndLiquidMeasure: { + categoryVolumeAndLiquidMeasure: { "l": 1, "L": 1, "lt": 1, @@ -2452,7 +2452,7 @@ var unitConversions = map[byte]map[string]float64{ "MTON": 8.82866668037215e-04, }, // conversion uses hectare (ha) as an intermediate unit - catgoryArea: { + categoryArea: { "ha": 1, "uk_acre": 2.47105381467165e+00, "us_acre": 2.47104393046628e+00, @@ -2480,12 +2480,12 @@ var unitConversions = map[byte]map[string]float64{ "yd^2": 1.19599004630108e+04, }, // conversion uses bit (bit) as an intermediate unit - catgoryInformation: { + categoryInformation: { "bit": 1, "byte": 0.125, }, // conversion uses Meters per Second (m/s) as an intermediate unit - catgorySpeed: { + categorySpeed: { "m/s": 1, "m/sec": 1, "m/h": 3.60e+03, @@ -2639,7 +2639,7 @@ func (fn *formulaFuncs) CONVERT(argsList *list.List) formulaArg { return newNumberFormulaArg(val / fromMultiplier) } else if fromUOM == toUOM { return newNumberFormulaArg(val / toMultiplier) - } else if fromCategory == catgoryTemperature { + } else if fromCategory == categoryTemperature { return newNumberFormulaArg(convertTemperature(fromUOM, toUOM, val)) } fromConversion := unitConversions[fromCategory][fromUOM] @@ -13607,7 +13607,7 @@ func (fn *formulaFuncs) replace(name string, argsList *list.List) formulaArg { if argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 4 arguments", name)) } - oldText, newText := argsList.Front().Value.(formulaArg).Value(), argsList.Back().Value.(formulaArg).Value() + sourceText, targetText := argsList.Front().Value.(formulaArg).Value(), argsList.Back().Value.(formulaArg).Value() startNumArg, numCharsArg := argsList.Front().Next().Value.(formulaArg).ToNumber(), argsList.Front().Next().Next().Value.(formulaArg).ToNumber() if startNumArg.Type != ArgNumber { return startNumArg @@ -13615,18 +13615,18 @@ func (fn *formulaFuncs) replace(name string, argsList *list.List) formulaArg { if numCharsArg.Type != ArgNumber { return numCharsArg } - oldTextLen, startIdx := len(oldText), int(startNumArg.Number) - if startIdx > oldTextLen { - startIdx = oldTextLen + 1 + sourceTextLen, startIdx := len(sourceText), int(startNumArg.Number) + if startIdx > sourceTextLen { + startIdx = sourceTextLen + 1 } endIdx := startIdx + int(numCharsArg.Number) - if endIdx > oldTextLen { - endIdx = oldTextLen + 1 + if endIdx > sourceTextLen { + endIdx = sourceTextLen + 1 } if startIdx < 1 || endIdx < 1 { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - result := oldText[:startIdx-1] + newText + oldText[endIdx-1:] + result := sourceText[:startIdx-1] + targetText + sourceText[endIdx-1:] return newStringFormulaArg(result) } @@ -13683,10 +13683,10 @@ func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg { if argsList.Len() != 3 && argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, "SUBSTITUTE requires 3 or 4 arguments") } - text, oldText := argsList.Front().Value.(formulaArg), argsList.Front().Next().Value.(formulaArg) - newText, instanceNum := argsList.Front().Next().Next().Value.(formulaArg), 0 + text, sourceText := argsList.Front().Value.(formulaArg), argsList.Front().Next().Value.(formulaArg) + targetText, instanceNum := argsList.Front().Next().Next().Value.(formulaArg), 0 if argsList.Len() == 3 { - return newStringFormulaArg(strings.ReplaceAll(text.Value(), oldText.Value(), newText.Value())) + return newStringFormulaArg(strings.ReplaceAll(text.Value(), sourceText.Value(), targetText.Value())) } instanceNumArg := argsList.Back().Value.(formulaArg).ToNumber() if instanceNumArg.Type != ArgNumber { @@ -13696,10 +13696,10 @@ func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg { if instanceNum < 1 { return newErrorFormulaArg(formulaErrorVALUE, "instance_num should be > 0") } - str, oldTextLen, count, chars, pos := text.Value(), len(oldText.Value()), instanceNum, 0, -1 + str, sourceTextLen, count, chars, pos := text.Value(), len(sourceText.Value()), instanceNum, 0, -1 for { count-- - index := strings.Index(str, oldText.Value()) + index := strings.Index(str, sourceText.Value()) if index == -1 { pos = -1 break @@ -13708,7 +13708,7 @@ func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg { if count == 0 { break } - idx := oldTextLen + index + idx := sourceTextLen + index chars += idx str = str[idx:] } @@ -13716,8 +13716,8 @@ func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg { if pos == -1 { return newStringFormulaArg(text.Value()) } - pre, post := text.Value()[:pos], text.Value()[pos+oldTextLen:] - return newStringFormulaArg(pre + newText.Value() + post) + pre, post := text.Value()[:pos], text.Value()[pos+sourceTextLen:] + return newStringFormulaArg(pre + targetText.Value() + post) } // TEXTJOIN function joins together a series of supplied text strings into one diff --git a/drawing.go b/drawing.go index 0bd8604b2a..4fe575bafa 100644 --- a/drawing.go +++ b/drawing.go @@ -91,7 +91,7 @@ func (f *File) addChart(opts *chartOptions, comboCharts []*chartOptions) { Cs: &aCs{ Typeface: "+mn-cs", }, - Latin: &aLatin{ + Latin: &xlsxCTTextFont{ Typeface: "+mn-lt", }, }, @@ -1168,7 +1168,7 @@ func (f *File) drawPlotAreaTxPr(opts *chartAxisOptions) *cTxPr { LumOff: &attrValInt{Val: intPtr(85000)}, }, }, - Latin: &aLatin{Typeface: "+mn-lt"}, + Latin: &xlsxCTTextFont{Typeface: "+mn-lt"}, Ea: &aEa{Typeface: "+mn-ea"}, Cs: &aCs{Typeface: "+mn-cs"}, }, diff --git a/errors.go b/errors.go index 6a23a2e954..f486ad4d15 100644 --- a/errors.go +++ b/errors.go @@ -93,6 +93,12 @@ func newStreamSetRowError(row int) error { return fmt.Errorf("row %d has already been written", row) } +// newViewIdxError defined the error message on receiving a invalid sheet view +// index. +func newViewIdxError(viewIndex int) error { + return fmt.Errorf("view index %d out of range", viewIndex) +} + var ( // ErrStreamSetColWidth defined the error message on set column width in // stream writing mode. diff --git a/file.go b/file.go index 1469af0977..fe5decaa0f 100644 --- a/file.go +++ b/file.go @@ -182,6 +182,7 @@ func (f *File) writeToZip(zw *zip.Writer) error { _ = f.sharedStringsLoader() f.sharedStringsWriter() f.styleSheetWriter() + f.themeWriter() for path, stream := range f.streams { fi, err := zw.Create(path) diff --git a/lib.go b/lib.go index 685571c487..16170a7183 100644 --- a/lib.go +++ b/lib.go @@ -626,12 +626,12 @@ func getXMLNamespace(space string, attr []xml.Attr) string { // replaceNameSpaceBytes provides a function to replace the XML root element // attribute by the given component part path and XML content. func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte { - oldXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) - newXmlns := []byte(templateNamespaceIDMap) + sourceXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) + targetXmlns := []byte(templateNamespaceIDMap) if attr, ok := f.xmlAttr[path]; ok { - newXmlns = []byte(genXMLNamespace(attr)) + targetXmlns = []byte(genXMLNamespace(attr)) } - return bytesReplace(contentMarshal, oldXmlns, bytes.ReplaceAll(newXmlns, []byte(" mc:Ignorable=\"r\""), []byte{}), -1) + return bytesReplace(contentMarshal, sourceXmlns, bytes.ReplaceAll(targetXmlns, []byte(" mc:Ignorable=\"r\""), []byte{}), -1) } // addNameSpaces provides a function to add an XML attribute by the given diff --git a/merge.go b/merge.go index 04dc493d70..a839b96df1 100644 --- a/merge.go +++ b/merge.go @@ -17,7 +17,11 @@ import "strings" func (mc *xlsxMergeCell) Rect() ([]int, error) { var err error if mc.rect == nil { - mc.rect, err = rangeRefToCoordinates(mc.Ref) + mergedCellsRef := mc.Ref + if !strings.Contains(mergedCellsRef, ":") { + mergedCellsRef += ":" + mergedCellsRef + } + mc.rect, err = rangeRefToCoordinates(mergedCellsRef) } return mc.rect, err } @@ -105,7 +109,11 @@ func (f *File) UnmergeCell(sheet, hCell, vCell string) error { if mergeCell == nil { continue } - rect2, _ := rangeRefToCoordinates(mergeCell.Ref) + mergedCellsRef := mergeCell.Ref + if !strings.Contains(mergedCellsRef, ":") { + mergedCellsRef += ":" + mergedCellsRef + } + rect2, _ := rangeRefToCoordinates(mergedCellsRef) if isOverlap(rect1, rect2) { continue } diff --git a/merge_test.go b/merge_test.go index 6977c5ac5c..e0b9210371 100644 --- a/merge_test.go +++ b/merge_test.go @@ -185,7 +185,7 @@ func TestUnmergeCell(t *testing.T) { ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} - assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), ErrParameterInvalid.Error()) + assert.NoError(t, f.UnmergeCell("Sheet1", "A2", "B3")) ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) @@ -194,6 +194,6 @@ func TestUnmergeCell(t *testing.T) { } func TestFlatMergedCells(t *testing.T) { - ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}} - assert.EqualError(t, flatMergedCells(ws, [][]*xlsxMergeCell{}), ErrParameterInvalid.Error()) + ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ""}}}} + assert.EqualError(t, flatMergedCells(ws, [][]*xlsxMergeCell{}), "cannot convert cell \"\" to coordinates: invalid cell name \"\"") } diff --git a/rows.go b/rows.go index 4f05f24314..5b21f29410 100644 --- a/rows.go +++ b/rows.go @@ -744,8 +744,8 @@ func checkRow(ws *xlsxWorksheet) error { } if colCount < lastCol { - oldList := rowData.C - newList := make([]xlsxC, 0, lastCol) + sourceList := rowData.C + targetList := make([]xlsxC, 0, lastCol) rowData.C = ws.SheetData.Row[rowIdx].C[:0] @@ -754,13 +754,13 @@ func checkRow(ws *xlsxWorksheet) error { if err != nil { return err } - newList = append(newList, xlsxC{R: cellName}) + targetList = append(targetList, xlsxC{R: cellName}) } - rowData.C = newList + rowData.C = targetList - for colIdx := range oldList { - colData := &oldList[colIdx] + for colIdx := range sourceList { + colData := &sourceList[colIdx] colNum, _, err := CellNameToCoordinates(colData.R) if err != nil { return err diff --git a/shape.go b/shape.go index e3c6c8bc17..9f250d79eb 100644 --- a/shape.go +++ b/shape.go @@ -418,7 +418,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOption AltLang: "en-US", U: u, Sz: p.Font.Size * 100, - Latin: &aLatin{Typeface: p.Font.Family}, + Latin: &xlsxCTTextFont{Typeface: p.Font.Family}, }, T: text, }, diff --git a/sheet.go b/sheet.go index ecd39f069a..070b47d119 100644 --- a/sheet.go +++ b/sheet.go @@ -243,9 +243,9 @@ func (f *File) relsWriter() { // strict requirements about the structure of the input XML. This function is // a horrible hack to fix that after the XML marshalling is completed. func replaceRelationshipsBytes(content []byte) []byte { - oldXmlns := []byte(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`) - newXmlns := []byte("r") - return bytesReplace(content, oldXmlns, newXmlns, -1) + sourceXmlns := []byte(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`) + targetXmlns := []byte("r") + return bytesReplace(content, sourceXmlns, targetXmlns, -1) } // SetActiveSheet provides a function to set the default active sheet of the @@ -1623,7 +1623,7 @@ func (f *File) InsertPageBreak(sheet, cell string) error { if row != 0 && rowBrk == -1 { ws.RowBreaks.Brk = append(ws.RowBreaks.Brk, &xlsxBrk{ ID: row, - Max: 16383, + Max: MaxColumns - 1, Man: true, }) ws.RowBreaks.ManualBreakCount++ @@ -1631,7 +1631,7 @@ func (f *File) InsertPageBreak(sheet, cell string) error { if col != 0 && colBrk == -1 { ws.ColBreaks.Brk = append(ws.ColBreaks.Brk, &xlsxBrk{ ID: col, - Max: 1048575, + Max: TotalRows - 1, Man: true, }) ws.ColBreaks.ManualBreakCount++ diff --git a/sheetview.go b/sheetview.go index a47d5100e5..9845942d3b 100644 --- a/sheetview.go +++ b/sheetview.go @@ -11,8 +11,6 @@ package excelize -import "fmt" - // getSheetView returns the SheetView object func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) { ws, err := f.workSheetReader(sheet) @@ -26,11 +24,11 @@ func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) } if viewIndex < 0 { if viewIndex < -len(ws.SheetViews.SheetView) { - return nil, fmt.Errorf("view index %d out of range", viewIndex) + return nil, newViewIdxError(viewIndex) } viewIndex = len(ws.SheetViews.SheetView) + viewIndex } else if viewIndex >= len(ws.SheetViews.SheetView) { - return nil, fmt.Errorf("view index %d out of range", viewIndex) + return nil, newViewIdxError(viewIndex) } return &(ws.SheetViews.SheetView[viewIndex]), err diff --git a/styles.go b/styles.go index 15de5f1ab1..f7d00e19a6 100644 --- a/styles.go +++ b/styles.go @@ -1057,6 +1057,15 @@ func (f *File) styleSheetWriter() { } } +// themeWriter provides a function to save xl/theme/theme1.xml after serialize +// structure. +func (f *File) themeWriter() { + if f.Theme != nil { + output, _ := xml.Marshal(f.Theme) + f.saveFileList(defaultXMLPathTheme, f.replaceNameSpaceBytes(defaultXMLPathTheme, output)) + } +} + // sharedStringsWriter provides a function to save xl/sharedStrings.xml after // serialize structure. func (f *File) sharedStringsWriter() { @@ -3311,11 +3320,11 @@ func getPaletteColor(color string) string { // themeReader provides a function to get the pointer to the xl/theme/theme1.xml // structure after deserialization. func (f *File) themeReader() *xlsxTheme { - var ( - err error - theme xlsxTheme - ) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/theme/theme1.xml")))). + if _, ok := f.Pkg.Load(defaultXMLPathTheme); !ok { + return nil + } + theme := xlsxTheme{XMLNSa: NameSpaceDrawingML.Value, XMLNSr: SourceRelationship.Value} + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathTheme)))). Decode(&theme); err != nil && err != io.EOF { log.Printf("xml decoder error: %s", err) } diff --git a/styles_test.go b/styles_test.go index f27c9a20e2..487a6df63e 100644 --- a/styles_test.go +++ b/styles_test.go @@ -334,8 +334,8 @@ func TestStylesReader(t *testing.T) { func TestThemeReader(t *testing.T) { f := NewFile() // Test read theme with unsupported charset. - f.Pkg.Store("xl/theme/theme1.xml", MacintoshCyrillicCharset) - assert.EqualValues(t, new(xlsxTheme), f.themeReader()) + f.Pkg.Store(defaultXMLPathTheme, MacintoshCyrillicCharset) + assert.EqualValues(t, &xlsxTheme{XMLNSa: NameSpaceDrawingML.Value, XMLNSr: SourceRelationship.Value}, f.themeReader()) } func TestSetCellStyle(t *testing.T) { diff --git a/templates.go b/templates.go index 1e46b56175..c8233c1830 100644 --- a/templates.go +++ b/templates.go @@ -21,6 +21,7 @@ const ( defaultXMLPathCalcChain = "xl/calcChain.xml" defaultXMLPathSharedStrings = "xl/sharedStrings.xml" defaultXMLPathStyles = "xl/styles.xml" + defaultXMLPathTheme = "xl/theme/theme1.xml" defaultXMLPathWorkbook = "xl/workbook.xml" defaultTempFileSST = "sharedStrings" ) diff --git a/xmlChart.go b/xmlChart.go index 2ebcdefe67..5165ea09ed 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -171,12 +171,11 @@ type aEa struct { Typeface string `xml:"typeface,attr"` } -// aLatin (Latin Font) directly maps the a:latin element. This element -// specifies that a Latin font be used for a specific run of text. This font is -// specified with a typeface attribute much like the others but is specifically -// classified as a Latin font. -type aLatin struct { - Typeface string `xml:"typeface,attr"` +type xlsxCTTextFont struct { + Typeface string `xml:"typeface,attr"` + Panose string `xml:"panose,attr,omitempty"` + PitchFamily string `xml:"pitchFamily,attr,omitempty"` + Charset string `xml:"Charset,attr,omitempty"` } // aR directly maps the a:r element. @@ -191,29 +190,29 @@ type aR struct { // properties are defined as direct formatting, since they are directly applied // to the run and supersede any formatting from styles. type aRPr struct { - AltLang string `xml:"altLang,attr,omitempty"` - B bool `xml:"b,attr"` - Baseline int `xml:"baseline,attr"` - Bmk string `xml:"bmk,attr,omitempty"` - Cap string `xml:"cap,attr,omitempty"` - Dirty bool `xml:"dirty,attr,omitempty"` - Err bool `xml:"err,attr,omitempty"` - I bool `xml:"i,attr"` - Kern int `xml:"kern,attr"` - Kumimoji bool `xml:"kumimoji,attr,omitempty"` - Lang string `xml:"lang,attr,omitempty"` - NoProof bool `xml:"noProof,attr,omitempty"` - NormalizeH bool `xml:"normalizeH,attr,omitempty"` - SmtClean bool `xml:"smtClean,attr,omitempty"` - SmtID uint64 `xml:"smtId,attr,omitempty"` - Spc int `xml:"spc,attr"` - Strike string `xml:"strike,attr,omitempty"` - Sz float64 `xml:"sz,attr,omitempty"` - U string `xml:"u,attr,omitempty"` - SolidFill *aSolidFill `xml:"a:solidFill"` - Latin *aLatin `xml:"a:latin"` - Ea *aEa `xml:"a:ea"` - Cs *aCs `xml:"a:cs"` + AltLang string `xml:"altLang,attr,omitempty"` + B bool `xml:"b,attr"` + Baseline int `xml:"baseline,attr"` + Bmk string `xml:"bmk,attr,omitempty"` + Cap string `xml:"cap,attr,omitempty"` + Dirty bool `xml:"dirty,attr,omitempty"` + Err bool `xml:"err,attr,omitempty"` + I bool `xml:"i,attr"` + Kern int `xml:"kern,attr"` + Kumimoji bool `xml:"kumimoji,attr,omitempty"` + Lang string `xml:"lang,attr,omitempty"` + NoProof bool `xml:"noProof,attr,omitempty"` + NormalizeH bool `xml:"normalizeH,attr,omitempty"` + SmtClean bool `xml:"smtClean,attr,omitempty"` + SmtID uint64 `xml:"smtId,attr,omitempty"` + Spc int `xml:"spc,attr"` + Strike string `xml:"strike,attr,omitempty"` + Sz float64 `xml:"sz,attr,omitempty"` + U string `xml:"u,attr,omitempty"` + SolidFill *aSolidFill `xml:"a:solidFill"` + Latin *xlsxCTTextFont `xml:"a:latin"` + Ea *aEa `xml:"a:ea"` + Cs *aCs `xml:"a:cs"` } // cSpPr (Shape Properties) directly maps the spPr element. This element diff --git a/xmlDrawing.go b/xmlDrawing.go index 5b4628b5e2..dc48ccc0b9 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -159,6 +159,7 @@ var IndexedColorMapping = []string{ "00CCFF", "CCFFFF", "CCFFCC", "FFFF99", "99CCFF", "FF99CC", "CC99FF", "FFCC99", "3366FF", "33CCCC", "99CC00", "FFCC00", "FF9900", "FF6600", "666699", "969696", "003366", "339966", "003300", "333300", "993300", "993366", "333399", "333333", + "000000", "FFFFFF", } // supportedImageTypes defined supported image types. diff --git a/xmlTheme.go b/xmlTheme.go index 6b9e207cb6..80bb3afafe 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -16,12 +16,59 @@ import "encoding/xml" // xlsxTheme directly maps the theme element in the namespace // http://schemas.openxmlformats.org/drawingml/2006/main type xlsxTheme struct { - ThemeElements xlsxThemeElements `xml:"themeElements"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/main theme"` + XMLNSa string `xml:"xmlns:a,attr"` + XMLNSr string `xml:"xmlns:r,attr"` + Name string `xml:"name,attr"` + ThemeElements xlsxBaseStyles `xml:"themeElements"` ObjectDefaults xlsxObjectDefaults `xml:"objectDefaults"` ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"extraClrSchemeLst"` + CustClrLst *xlsxInnerXML `xml:"custClrLst"` ExtLst *xlsxExtLst `xml:"extLst"` } +// xlsxBaseStyles defines the theme elements for a theme, and is the workhorse +// of the theme. The bulk of the shared theme information that is used by a +// given document is defined here. Within this complex type is defined a color +// scheme, a font scheme, and a style matrix (format scheme) that defines +// different formatting options for different pieces of a document. +type xlsxBaseStyles struct { + ClrScheme xlsxColorScheme `xml:"clrScheme"` + FontScheme xlsxFontScheme `xml:"fontScheme"` + FmtScheme xlsxStyleMatrix `xml:"fmtScheme"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxCTColor holds the actual color values that are to be applied to a given +// diagram and how those colors are to be applied. +type xlsxCTColor struct { + ScrgbClr *xlsxInnerXML `xml:"scrgbClr"` + SrgbClr *attrValString `xml:"srgbClr"` + HslClr *xlsxInnerXML `xml:"hslClr"` + SysClr *xlsxSysClr `xml:"sysClr"` + SchemeClr *xlsxInnerXML `xml:"schemeClr"` + PrstClr *xlsxInnerXML `xml:"prstClr"` +} + +// xlsxColorScheme defines a set of colors for the theme. The set of colors +// consists of twelve color slots that can each hold a color of choice. +type xlsxColorScheme struct { + Name string `xml:"name,attr"` + Dk1 xlsxCTColor `xml:"dk1"` + Lt1 xlsxCTColor `xml:"lt1"` + Dk2 xlsxCTColor `xml:"dk2"` + Lt2 xlsxCTColor `xml:"lt2"` + Accent1 xlsxCTColor `xml:"accent1"` + Accent2 xlsxCTColor `xml:"accent2"` + Accent3 xlsxCTColor `xml:"accent3"` + Accent4 xlsxCTColor `xml:"accent4"` + Accent5 xlsxCTColor `xml:"accent5"` + Accent6 xlsxCTColor `xml:"accent6"` + Hlink xlsxCTColor `xml:"hlink"` + FolHlink xlsxCTColor `xml:"folHlink"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + // objectDefaults element allows for the definition of default shape, line, // and textbox formatting properties. An application can use this information // to format a shape (or text) initially on insertion into a document. @@ -35,24 +82,24 @@ type xlsxExtraClrSchemeLst struct { ExtraClrSchemeLst string `xml:",innerxml"` } -// xlsxThemeElements directly maps the element defines the theme formatting -// options for the theme and is the workhorse of the theme. This is where the -// bulk of the shared theme information is contained and used by a document. -// This element contains the color scheme, font scheme, and format scheme -// elements which define the different formatting aspects of what a theme -// defines. -type xlsxThemeElements struct { - ClrScheme xlsxClrScheme `xml:"clrScheme"` - FontScheme xlsxFontScheme `xml:"fontScheme"` - FmtScheme xlsxFmtScheme `xml:"fmtScheme"` +// xlsxCTSupplementalFont defines an additional font that is used for language +// specific fonts in themes. For example, one can specify a font that gets used +// only within the Japanese language context. +type xlsxCTSupplementalFont struct { + Script string `xml:"script,attr"` + Typeface string `xml:"typeface,attr"` } -// xlsxClrScheme element specifies the theme color, stored in the document's -// Theme part to which the value of this theme color shall be mapped. This -// mapping enables multiple theme colors to be chained together. -type xlsxClrScheme struct { - Name string `xml:"name,attr"` - Children []xlsxClrSchemeEl `xml:",any"` +// xlsxFontCollection defines a major and minor font which is used in the font +// scheme. A font collection consists of a font definition for Latin, East +// Asian, and complex script. On top of these three definitions, one can also +// define a font for use in a specific language or languages. +type xlsxFontCollection struct { + Latin *xlsxCTTextFont `xml:"latin"` + Ea *xlsxCTTextFont `xml:"ea"` + Cs *xlsxCTTextFont `xml:"cs"` + Font []xlsxCTSupplementalFont `xml:"font"` + ExtLst *xlsxExtLst `xml:"extLst"` } // xlsxFontScheme element defines the font scheme within the theme. The font @@ -61,34 +108,19 @@ type xlsxClrScheme struct { // document, and the minor font corresponds well with the normal text or // paragraph areas. type xlsxFontScheme struct { - Name string `xml:"name,attr"` - MajorFont xlsxMajorFont `xml:"majorFont"` - MinorFont xlsxMinorFont `xml:"minorFont"` - ExtLst *xlsxExtLst `xml:"extLst"` -} - -// xlsxMajorFont element defines the set of major fonts which are to be used -// under different languages or locals. -type xlsxMajorFont struct { - Children []xlsxFontSchemeEl `xml:",any"` -} - -// xlsxMinorFont element defines the set of minor fonts that are to be used -// under different languages or locals. -type xlsxMinorFont struct { - Children []xlsxFontSchemeEl `xml:",any"` -} - -// xlsxFmtScheme element contains the background fill styles, effect styles, -// fill styles, and line styles which define the style matrix for a theme. The -// style matrix consists of subtle, moderate, and intense fills, lines, and -// effects. The background fills are not generally thought of to directly be -// associated with the matrix, but do play a role in the style of the overall -// document. Usually, a given object chooses a single line style, a single -// fill style, and a single effect style in order to define the overall final -// look of the object. -type xlsxFmtScheme struct { - Name string `xml:"name,attr"` + Name string `xml:"name,attr"` + MajorFont xlsxFontCollection `xml:"majorFont"` + MinorFont xlsxFontCollection `xml:"minorFont"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxStyleMatrix defines a set of formatting options, which can be referenced +// by documents that apply a certain style to a given part of an object. For +// example, in a given shape, say a rectangle, one can reference a themed line +// style, themed effect, and themed fill that would be theme specific and +// change when the theme is changed. +type xlsxStyleMatrix struct { + Name string `xml:"name,attr,omitempty"` FillStyleLst xlsxFillStyleLst `xml:"fillStyleLst"` LnStyleLst xlsxLnStyleLst `xml:"lnStyleLst"` EffectStyleLst xlsxEffectStyleLst `xml:"effectStyleLst"` @@ -123,26 +155,6 @@ type xlsxBgFillStyleLst struct { BgFillStyleLst string `xml:",innerxml"` } -// xlsxClrScheme specifies the theme color, stored in the document's Theme -// part to which the value of this theme color shall be mapped. This mapping -// enables multiple theme colors to be chained together. -type xlsxClrSchemeEl struct { - XMLName xml.Name - SysClr *xlsxSysClr `xml:"sysClr"` - SrgbClr *attrValString `xml:"srgbClr"` -} - -// xlsxFontSchemeEl directly maps the major and minor font of the style's font -// scheme. -type xlsxFontSchemeEl struct { - XMLName xml.Name - Script string `xml:"script,attr,omitempty"` - Typeface string `xml:"typeface,attr"` - Panose string `xml:"panose,attr,omitempty"` - PitchFamily string `xml:"pitchFamily,attr,omitempty"` - Charset string `xml:"charset,attr,omitempty"` -} - // xlsxSysClr element specifies a color bound to predefined operating system // elements. type xlsxSysClr struct { From db2d084ada1a08a48967506b2f1852062168deec Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 2 Nov 2022 08:42:00 +0800 Subject: [PATCH 124/213] This closes #1204, breaking changes for add comments - Allowing insert SVG format images - Unit tests updated --- .gitignore | 18 +++--- cell.go | 55 ++++++++++-------- comment.go | 137 ++++++++++++++++++++++---------------------- comment_test.go | 22 +++---- excelize_test.go | 3 +- picture.go | 23 +++++++- picture_test.go | 4 +- xmlComments.go | 15 ++--- xmlDrawing.go | 37 ++++++++++-- xmlSharedStrings.go | 4 +- 10 files changed, 181 insertions(+), 137 deletions(-) diff --git a/.gitignore b/.gitignore index 44b8b09b45..8bf9e7f5b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,15 @@ +.DS_Store +.idea +*.json +*.out +*.test ~$*.xlsx +test/*.png +test/BadWorkbook.SaveAsEmptyStruct.xlsx +test/Encryption*.xlsx +test/excelize-* test/Test*.xlam test/Test*.xlsm test/Test*.xlsx test/Test*.xltm test/Test*.xltx -# generated files -test/Encryption*.xlsx -test/BadWorkbook.SaveAsEmptyStruct.xlsx -test/*.png -test/excelize-* -*.out -*.test -.idea -.DS_Store diff --git a/cell.go b/cell.go index fbc84b7d05..eb604419f1 100644 --- a/cell.go +++ b/cell.go @@ -902,31 +902,7 @@ func getCellRichText(si *xlsxSI) (runs []RichTextRun) { Text: v.T.Val, } if v.RPr != nil { - font := Font{Underline: "none"} - font.Bold = v.RPr.B != nil - font.Italic = v.RPr.I != nil - if v.RPr.U != nil { - font.Underline = "single" - if v.RPr.U.Val != nil { - font.Underline = *v.RPr.U.Val - } - } - if v.RPr.RFont != nil && v.RPr.RFont.Val != nil { - font.Family = *v.RPr.RFont.Val - } - if v.RPr.Sz != nil && v.RPr.Sz.Val != nil { - font.Size = *v.RPr.Sz.Val - } - font.Strike = v.RPr.Strike != nil - if v.RPr.Color != nil { - font.Color = strings.TrimPrefix(v.RPr.Color.RGB, "FF") - 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 + run.Font = newFont(v.RPr) } runs = append(runs, run) } @@ -985,6 +961,35 @@ func newRpr(fnt *Font) *xlsxRPr { return &rpr } +// newFont create font format by given run properties for the rich text. +func newFont(rPr *xlsxRPr) *Font { + font := Font{Underline: "none"} + font.Bold = rPr.B != nil + font.Italic = rPr.I != nil + if rPr.U != nil { + font.Underline = "single" + if rPr.U.Val != nil { + font.Underline = *rPr.U.Val + } + } + if rPr.RFont != nil && rPr.RFont.Val != nil { + font.Family = *rPr.RFont.Val + } + if rPr.Sz != nil && rPr.Sz.Val != nil { + font.Size = *rPr.Sz.Val + } + font.Strike = rPr.Strike != nil + if rPr.Color != nil { + font.Color = strings.TrimPrefix(rPr.Color.RGB, "FF") + if rPr.Color.Theme != nil { + font.ColorTheme = rPr.Color.Theme + } + font.ColorIndexed = rPr.Color.Indexed + font.ColorTint = rPr.Color.Tint + } + return &font +} + // setRichText provides a function to set rich text of a cell. func setRichText(runs []RichTextRun) ([]xlsxR, error) { var ( diff --git a/comment.go b/comment.go index 3d0832469e..28c6cf82fc 100644 --- a/comment.go +++ b/comment.go @@ -13,7 +13,6 @@ package excelize import ( "bytes" - "encoding/json" "encoding/xml" "fmt" "io" @@ -23,17 +22,6 @@ import ( "strings" ) -// parseCommentOptions provides a function to parse the format settings of -// the comment with default value. -func parseCommentOptions(opts string) (*commentOptions, error) { - options := commentOptions{ - Author: "Author:", - Text: " ", - } - err := json.Unmarshal([]byte(opts), &options) - return &options, err -} - // GetComments retrieves all comments and returns a map of worksheet name to // the worksheet comments. func (f *File) GetComments() (comments map[string][]Comment) { @@ -53,14 +41,18 @@ func (f *File) GetComments() (comments map[string][]Comment) { if comment.AuthorID < len(d.Authors.Author) { sheetComment.Author = d.Authors.Author[comment.AuthorID] } - sheetComment.Ref = comment.Ref + sheetComment.Cell = comment.Ref sheetComment.AuthorID = comment.AuthorID if comment.Text.T != nil { sheetComment.Text += *comment.Text.T } for _, text := range comment.Text.R { if text.T != nil { - sheetComment.Text += text.T.Val + run := RichTextRun{Text: text.T.Val} + if text.RPr != nil { + run.Font = newFont(text.RPr) + } + sheetComment.Runs = append(sheetComment.Runs, run) } } sheetComments = append(sheetComments, sheetComment) @@ -92,12 +84,15 @@ func (f *File) getSheetComments(sheetFile string) string { // author length is 255 and the max text length is 32512. For example, add a // comment in Sheet1!$A$30: // -// err := f.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`) -func (f *File) AddComment(sheet, cell, opts string) error { - options, err := parseCommentOptions(opts) - if err != nil { - return err - } +// err := f.AddComment(sheet, excelize.Comment{ +// Cell: "A12", +// Author: "Excelize", +// Runs: []excelize.RichTextRun{ +// {Text: "Excelize: ", Font: &excelize.Font{Bold: true}}, +// {Text: "This is a comment."}, +// }, +// }) +func (f *File) AddComment(sheet string, comment Comment) error { // Read sheet data. ws, err := f.workSheetReader(sheet) if err != nil { @@ -122,20 +117,19 @@ func (f *File) AddComment(sheet, cell, opts string) error { f.addSheetLegacyDrawing(sheet, rID) } commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml" - var colCount int - for i, l := range strings.Split(options.Text, "\n") { - if ll := len(l); ll > colCount { - if i == 0 { - ll += len(options.Author) + var rows, cols int + for _, runs := range comment.Runs { + for _, subStr := range strings.Split(runs.Text, "\n") { + rows++ + if chars := len(subStr); chars > cols { + cols = chars } - colCount = ll } } - err = f.addDrawingVML(commentID, drawingVML, cell, strings.Count(options.Text, "\n")+1, colCount) - if err != nil { + if err = f.addDrawingVML(commentID, drawingVML, comment.Cell, rows+1, cols); err != nil { return err } - f.addComment(commentsXML, cell, options) + f.addComment(commentsXML, comment) f.addContentTypePart(commentID, "comments") return err } @@ -280,56 +274,59 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, // addComment provides a function to create chart as xl/comments%d.xml by // given cell and format sets. -func (f *File) addComment(commentsXML, cell string, opts *commentOptions) { - a := opts.Author - t := opts.Text - if len(a) > MaxFieldLength { - a = a[:MaxFieldLength] +func (f *File) addComment(commentsXML string, comment Comment) { + if comment.Author == "" { + comment.Author = "Author" } - if len(t) > 32512 { - t = t[:32512] + if len(comment.Author) > MaxFieldLength { + comment.Author = comment.Author[:MaxFieldLength] } - comments := f.commentsReader(commentsXML) - authorID := 0 + comments, authorID := f.commentsReader(commentsXML), 0 if comments == nil { - comments = &xlsxComments{Authors: xlsxAuthor{Author: []string{opts.Author}}} + comments = &xlsxComments{Authors: xlsxAuthor{Author: []string{comment.Author}}} } - if inStrSlice(comments.Authors.Author, opts.Author, true) == -1 { - comments.Authors.Author = append(comments.Authors.Author, opts.Author) + if inStrSlice(comments.Authors.Author, comment.Author, true) == -1 { + comments.Authors.Author = append(comments.Authors.Author, comment.Author) authorID = len(comments.Authors.Author) - 1 } - defaultFont := f.GetDefaultFont() - bold := "" - cmt := xlsxComment{ - Ref: cell, + defaultFont, chars, cmt := f.GetDefaultFont(), 0, xlsxComment{ + Ref: comment.Cell, AuthorID: authorID, - Text: xlsxText{ - R: []xlsxR{ - { - RPr: &xlsxRPr{ - B: &bold, - Sz: &attrValFloat{Val: float64Ptr(9)}, - Color: &xlsxColor{ - Indexed: 81, - }, - RFont: &attrValString{Val: stringPtr(defaultFont)}, - Family: &attrValInt{Val: intPtr(2)}, - }, - T: &xlsxT{Val: a}, - }, - { - RPr: &xlsxRPr{ - Sz: &attrValFloat{Val: float64Ptr(9)}, - Color: &xlsxColor{ - Indexed: 81, - }, - RFont: &attrValString{Val: stringPtr(defaultFont)}, - Family: &attrValInt{Val: intPtr(2)}, - }, - T: &xlsxT{Val: t}, + Text: xlsxText{R: []xlsxR{}}, + } + if comment.Text != "" { + if len(comment.Text) > TotalCellChars { + comment.Text = comment.Text[:TotalCellChars] + } + cmt.Text.T = stringPtr(comment.Text) + chars += len(comment.Text) + } + for _, run := range comment.Runs { + if chars == TotalCellChars { + break + } + if chars+len(run.Text) > TotalCellChars { + run.Text = run.Text[:TotalCellChars-chars] + } + chars += len(run.Text) + r := xlsxR{ + RPr: &xlsxRPr{ + Sz: &attrValFloat{Val: float64Ptr(9)}, + Color: &xlsxColor{ + Indexed: 81, }, + RFont: &attrValString{Val: stringPtr(defaultFont)}, + Family: &attrValInt{Val: intPtr(2)}, }, - }, + T: &xlsxT{Val: run.Text, Space: xml.Attr{ + Name: xml.Name{Space: NameSpaceXML, Local: "space"}, + Value: "preserve", + }}, + } + if run.Font != nil { + r.RPr = newRpr(run.Font) + } + cmt.Text.R = append(cmt.Text.R, r) } comments.CommentList.Comment = append(comments.CommentList.Comment, cmt) f.Comments[commentsXML] = comments diff --git a/comment_test.go b/comment_test.go index 2beca70c2e..019dc3b8ed 100644 --- a/comment_test.go +++ b/comment_test.go @@ -26,14 +26,14 @@ func TestAddComments(t *testing.T) { t.FailNow() } - s := strings.Repeat("c", 32768) - assert.NoError(t, f.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`)) - assert.NoError(t, f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`)) + s := strings.Repeat("c", TotalCellChars+1) + assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A30", Author: s, Text: s, Runs: []RichTextRun{{Text: s}, {Text: s}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "B7", Author: "Excelize", Text: s[:TotalCellChars-1], Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}})) // Test add comment on not exists worksheet. - assert.EqualError(t, f.AddComment("SheetN", "B7", `{"author":"Excelize: ","text":"This is a comment."}`), "sheet SheetN does not exist") + assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist") // Test add comment on with illegal cell reference - assert.EqualError(t, f.AddComment("Sheet1", "A", `{"author":"Excelize: ","text":"This is a comment."}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { assert.Len(t, f.GetComments(), 2) } @@ -52,12 +52,12 @@ func TestDeleteComment(t *testing.T) { t.FailNow() } - assert.NoError(t, f.AddComment("Sheet2", "A40", `{"author":"Excelize: ","text":"This is a comment1."}`)) - assert.NoError(t, f.AddComment("Sheet2", "A41", `{"author":"Excelize: ","text":"This is a comment2."}`)) - assert.NoError(t, f.AddComment("Sheet2", "C41", `{"author":"Excelize: ","text":"This is a comment3."}`)) - assert.NoError(t, f.AddComment("Sheet2", "C41", `{"author":"Excelize: ","text":"This is a comment3-1."}`)) - assert.NoError(t, f.AddComment("Sheet2", "C42", `{"author":"Excelize: ","text":"This is a comment4."}`)) - assert.NoError(t, f.AddComment("Sheet2", "C41", `{"author":"Excelize: ","text":"This is a comment3-2."}`)) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A40", Text: "Excelize: This is a comment1."})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3-1."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C42", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment4."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}})) assert.NoError(t, f.DeleteComment("Sheet2", "A40")) diff --git a/excelize_test.go b/excelize_test.go index 4c86d56005..74895f5369 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -946,8 +946,7 @@ func TestSetDeleteSheet(t *testing.T) { t.FailNow() } f.DeleteSheet("Sheet1") - assert.EqualError(t, f.AddComment("Sheet1", "A1", ""), "unexpected end of JSON input") - assert.NoError(t, f.AddComment("Sheet1", "A1", `{"author":"Excelize: ","text":"This is a comment."}`)) + assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A1", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx"))) }) } diff --git a/picture.go b/picture.go index 05e4a51644..a7c1edb217 100644 --- a/picture.go +++ b/picture.go @@ -183,7 +183,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, opts, name, extension string, fi drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, options.Hyperlink, hyperlinkType) } ws.Unlock() - err = f.addDrawingPicture(sheet, drawingXML, cell, name, img.Width, img.Height, drawingRID, drawingHyperlinkRID, options) + err = f.addDrawingPicture(sheet, drawingXML, cell, name, ext, drawingRID, drawingHyperlinkRID, img, options) if err != nil { return err } @@ -263,11 +263,12 @@ func (f *File) countDrawings() (count int) { // addDrawingPicture provides a function to add picture by given sheet, // drawingXML, cell, file name, width, height relationship index and format // sets. -func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, height, rID, hyperlinkRID int, opts *pictureOptions) error { +func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, hyperlinkRID int, img image.Config, opts *pictureOptions) error { col, row, err := CellNameToCoordinates(cell) if err != nil { return err } + width, height := img.Width, img.Height if opts.Autofit { width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts) if err != nil { @@ -308,6 +309,19 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he } pic.BlipFill.Blip.R = SourceRelationship.Value pic.BlipFill.Blip.Embed = "rId" + strconv.Itoa(rID) + if ext == ".svg" { + pic.BlipFill.Blip.ExtList = &xlsxEGOfficeArtExtensionList{ + Ext: []xlsxCTOfficeArtExtension{ + { + URI: ExtURISVG, + SVGBlip: xlsxCTSVGBlip{ + XMLNSaAVG: NameSpaceDrawing2016SVG.Value, + Embed: pic.BlipFill.Blip.Embed, + }, + }, + }, + } + } pic.SpPr.PrstGeom.Prst = "rect" twoCellAnchor.Pic = &pic @@ -362,7 +376,10 @@ func (f *File) addMedia(file []byte, ext string) string { // setContentTypePartImageExtensions provides a function to set the content // type for relationship parts and the Main Document part. func (f *File) setContentTypePartImageExtensions() { - imageTypes := map[string]string{"jpeg": "image/", "png": "image/", "gif": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-", "emz": "image/x-", "wmz": "image/x-"} + imageTypes := map[string]string{ + "jpeg": "image/", "png": "image/", "gif": "image/", "svg": "image/", "tiff": "image/", + "emf": "image/x-", "wmf": "image/x-", "emz": "image/x-", "wmz": "image/x-", + } content := f.contentTypesReader() content.Lock() defer content.Unlock() diff --git a/picture_test.go b/picture_test.go index e90de20a35..c34780f719 100644 --- a/picture_test.go +++ b/picture_test.go @@ -90,10 +90,12 @@ func TestAddPictureErrors(t *testing.T) { image.RegisterFormat("wmf", "", decode, decodeConfig) image.RegisterFormat("emz", "", decode, decodeConfig) image.RegisterFormat("wmz", "", decode, decodeConfig) + image.RegisterFormat("svg", "", decode, decodeConfig) assert.NoError(t, f.AddPicture("Sheet1", "Q1", filepath.Join("test", "images", "excel.emf"), "")) assert.NoError(t, f.AddPicture("Sheet1", "Q7", filepath.Join("test", "images", "excel.wmf"), "")) assert.NoError(t, f.AddPicture("Sheet1", "Q13", filepath.Join("test", "images", "excel.emz"), "")) assert.NoError(t, f.AddPicture("Sheet1", "Q19", filepath.Join("test", "images", "excel.wmz"), "")) + assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", `{"x_scale": 2.1}`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx"))) assert.NoError(t, f.Close()) } @@ -175,7 +177,7 @@ func TestGetPicture(t *testing.T) { func TestAddDrawingPicture(t *testing.T) { // Test addDrawingPicture with illegal cell reference. f := NewFile() - assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, 0, 0, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", "", 0, 0, image.Config{}, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestAddPictureFromBytes(t *testing.T) { diff --git a/xmlComments.go b/xmlComments.go index 731f416aaa..7b67e67823 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -72,16 +72,11 @@ type xlsxPhoneticRun struct { T string `xml:"t"` } -// commentOptions directly maps the format settings of the comment. -type commentOptions struct { - Author string `json:"author"` - Text string `json:"text"` -} - // Comment directly maps the comment information. type Comment struct { - Author string `json:"author"` - AuthorID int `json:"author_id"` - Ref string `json:"ref"` - Text string `json:"text"` + Author string `json:"author"` + AuthorID int `json:"author_id"` + Cell string `json:"cell"` + Text string `json:"string"` + Runs []RichTextRun `json:"runs"` } diff --git a/xmlDrawing.go b/xmlDrawing.go index dc48ccc0b9..56ddc0e780 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -29,6 +29,7 @@ var ( NameSpaceDrawingML = xml.Attr{Name: xml.Name{Local: "a", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/main"} NameSpaceDrawingMLChart = xml.Attr{Name: xml.Name{Local: "c", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/chart"} NameSpaceDrawingMLSpreadSheet = xml.Attr{Name: xml.Name{Local: "xdr", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"} + NameSpaceDrawing2016SVG = xml.Attr{Name: xml.Name{Local: "asvg", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main"} NameSpaceSpreadSheetX15 = xml.Attr{Name: xml.Name{Local: "x15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"} NameSpaceSpreadSheetExcel2006Main = xml.Attr{Name: xml.Name{Local: "xne", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/excel/2006/main"} NameSpaceMacExcel2008Main = xml.Attr{Name: xml.Name{Local: "mx", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/mac/excel/2008/main"} @@ -95,6 +96,7 @@ const ( ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}" ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}" ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}" + ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}" ) // Excel specifications and limits @@ -163,7 +165,11 @@ var IndexedColorMapping = []string{ } // supportedImageTypes defined supported image types. -var supportedImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff", ".emf": ".emf", ".wmf": ".wmf", ".emz": ".emz", ".wmz": ".wmz"} +var supportedImageTypes = map[string]string{ + ".emf": ".emf", ".emz": ".emz", ".gif": ".gif", ".jpeg": ".jpeg", + ".jpg": ".jpeg", ".png": ".png", ".svg": ".svg", ".tif": ".tiff", + ".tiff": ".tiff", ".wmf": ".wmf", ".wmz": ".wmz", +} // supportedContentTypes defined supported file format types. var supportedContentTypes = map[string]string{ @@ -231,9 +237,10 @@ type xlsxPicLocks struct { // xlsxBlip element specifies the existence of an image (binary large image or // picture) and contains a reference to the image data. type xlsxBlip struct { - Embed string `xml:"r:embed,attr"` - Cstate string `xml:"cstate,attr,omitempty"` - R string `xml:"xmlns:r,attr"` + Embed string `xml:"r:embed,attr"` + Cstate string `xml:"cstate,attr,omitempty"` + R string `xml:"xmlns:r,attr"` + ExtList *xlsxEGOfficeArtExtensionList `xml:"a:extLst"` } // xlsxStretch directly maps the stretch element. This element specifies that a @@ -293,6 +300,28 @@ type xlsxNvPicPr struct { CNvPicPr xlsxCNvPicPr `xml:"xdr:cNvPicPr"` } +// xlsxCTSVGBlip specifies a graphic element in Scalable Vector Graphics (SVG) +// format. +type xlsxCTSVGBlip struct { + XMLNSaAVG string `xml:"xmlns:asvg,attr"` + Embed string `xml:"r:embed,attr"` + Link string `xml:"r:link,attr,omitempty"` +} + +// xlsxCTOfficeArtExtension used for future extensibility and is seen elsewhere +// throughout the drawing area. +type xlsxCTOfficeArtExtension struct { + XMLName xml.Name `xml:"a:ext"` + URI string `xml:"uri,attr"` + SVGBlip xlsxCTSVGBlip `xml:"asvg:svgBlip"` +} + +// xlsxEGOfficeArtExtensionList used for future extensibility and is seen +// elsewhere throughout the drawing area. +type xlsxEGOfficeArtExtensionList struct { + Ext []xlsxCTOfficeArtExtension `xml:"ext"` +} + // xlsxBlipFill directly maps the blipFill (Picture Fill). This element // specifies the kind of picture fill that the picture object has. Because a // picture has a picture fill already by default, it is possible to have two diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 3249ecacf7..7dac544236 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -83,6 +83,6 @@ type xlsxRPr struct { // RichTextRun directly maps the settings of the rich text run. type RichTextRun struct { - Font *Font - Text string + Font *Font `json:"font"` + Text string `json:"text"` } From 4998b7b92980e1139b3f38d3c2b8cbc11b1a629d Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 3 Nov 2022 00:23:48 +0800 Subject: [PATCH 125/213] This closes #1383, skip empty rows when saving the spreadsheet to reduce file size --- rows.go | 7 +++++++ sheet.go | 27 +++++++++++++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/rows.go b/rows.go index 5b21f29410..bfea398f97 100644 --- a/rows.go +++ b/rows.go @@ -772,6 +772,13 @@ func checkRow(ws *xlsxWorksheet) error { return nil } +// hasAttr determine if row non-default attributes. +func (r *xlsxRow) hasAttr() bool { + return r.Spans != "" || r.S != 0 || r.CustomFormat || r.Ht != 0 || + r.Hidden || r.CustomHeight || r.OutlineLevel != 0 || r.Collapsed || + r.ThickTop || r.ThickBot || r.Ph +} + // SetRowStyle provides a function to set the style of rows by given worksheet // name, row range, and style ID. Note that this will overwrite the existing // styles for the rows, it won't append or merge style with existing styles. diff --git a/sheet.go b/sheet.go index 070b47d119..1346801f77 100644 --- a/sheet.go +++ b/sheet.go @@ -139,9 +139,11 @@ func (f *File) mergeExpandedCols(ws *xlsxWorksheet) { // workSheetWriter provides a function to save xl/worksheets/sheet%d.xml after // serialize structure. func (f *File) workSheetWriter() { - var arr []byte - buffer := bytes.NewBuffer(arr) - encoder := xml.NewEncoder(buffer) + var ( + arr []byte + buffer = bytes.NewBuffer(arr) + encoder = xml.NewEncoder(buffer) + ) f.Sheet.Range(func(p, ws interface{}) bool { if ws != nil { sheet := ws.(*xlsxWorksheet) @@ -151,9 +153,7 @@ func (f *File) workSheetWriter() { if sheet.Cols != nil && len(sheet.Cols.Col) > 0 { f.mergeExpandedCols(sheet) } - for k, v := range sheet.SheetData.Row { - sheet.SheetData.Row[k].C = trimCell(v.C) - } + sheet.SheetData.Row = trimRow(&sheet.SheetData) if sheet.SheetPr != nil || sheet.Drawing != nil || sheet.Hyperlinks != nil || sheet.Picture != nil || sheet.TableParts != nil { f.addNameSpaces(p.(string), SourceRelationship) } @@ -178,6 +178,21 @@ func (f *File) workSheetWriter() { }) } +// trimRow provides a function to trim empty rows. +func trimRow(sheetData *xlsxSheetData) []xlsxRow { + var ( + row xlsxRow + rows []xlsxRow + ) + for k, v := range sheetData.Row { + row = sheetData.Row[k] + if row.C = trimCell(v.C); len(row.C) != 0 || row.hasAttr() { + rows = append(rows, row) + } + } + return rows +} + // trimCell provides a function to trim blank cells which created by fillColumns. func trimCell(column []xlsxC) []xlsxC { rowFull := true From 75c912ca952bf47bbe421030554ef580ff4f3996 Mon Sep 17 00:00:00 2001 From: Martin Martinez Rivera Date: Fri, 4 Nov 2022 21:41:07 -0700 Subject: [PATCH 126/213] This closes #1384, fix segmentation fault in `formattedValue` (#1385) - Add nil pointer guard in cell format - Add tests to verify the nil checks in formattedValue Co-authored-by: Zach Clark --- cell.go | 5 ++++- cell_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/cell.go b/cell.go index eb604419f1..ebf4681fe3 100644 --- a/cell.go +++ b/cell.go @@ -1292,6 +1292,9 @@ func (f *File) formattedValue(s int, v string, raw bool) string { return v } styleSheet := f.stylesReader() + if styleSheet.CellXfs == nil { + return v + } if s >= len(styleSheet.CellXfs.Xf) { return v } @@ -1306,7 +1309,7 @@ func (f *File) formattedValue(s int, v string, raw bool) string { if ok := builtInNumFmtFunc[numFmtID]; ok != nil { return ok(v, builtInNumFmt[numFmtID], date1904) } - if styleSheet == nil || styleSheet.NumFmts == nil { + if styleSheet.NumFmts == nil { return v } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { diff --git a/cell_test.go b/cell_test.go index f7412111d4..6689c36aff 100644 --- a/cell_test.go +++ b/cell_test.go @@ -744,6 +744,35 @@ func TestFormattedValue2(t *testing.T) { } } +func TestFormattedValueNilXfs(t *testing.T) { + // Set the CellXfs to nil and verify that the formattedValue function does not crash. + f := NewFile() + f.Styles.CellXfs = nil + assert.Equal(t, "43528", f.formattedValue(3, "43528", false)) +} + +func TestFormattedValueNilNumFmts(t *testing.T) { + // Set the NumFmts value to nil and verify that the formattedValue function does not crash. + f := NewFile() + f.Styles.NumFmts = nil + assert.Equal(t, "43528", f.formattedValue(3, "43528", false)) +} + +func TestFormattedValueNilWorkbook(t *testing.T) { + // Set the Workbook value to nil and verify that the formattedValue function does not crash. + f := NewFile() + f.WorkBook = nil + assert.Equal(t, "43528", f.formattedValue(3, "43528", false)) +} + +func TestFormattedValueNilWorkbookPr(t *testing.T) { + // Set the WorkBook.WorkbookPr value to nil and verify that the formattedValue function does not + // crash. + f := NewFile() + f.WorkBook.WorkbookPr = nil + assert.Equal(t, "43528", f.formattedValue(3, "43528", false)) +} + func TestSharedStringsError(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128}) assert.NoError(t, err) From 8753950d62c150034a919599a7762cef19035552 Mon Sep 17 00:00:00 2001 From: March <115345952+March0715@users.noreply.github.com> Date: Tue, 8 Nov 2022 00:35:19 +0800 Subject: [PATCH 127/213] Delete shared formula in calc chain when writing a formula cell (#1387) --- cell.go | 4 +++- sheetpr.go | 54 ++++++++++++++++-------------------------------------- 2 files changed, 19 insertions(+), 39 deletions(-) diff --git a/cell.go b/cell.go index ebf4681fe3..c8fa9b2516 100644 --- a/cell.go +++ b/cell.go @@ -175,13 +175,15 @@ func (c *xlsxC) hasValue() bool { // removeFormula delete formula for the cell. func (f *File) removeFormula(c *xlsxC, ws *xlsxWorksheet, sheet string) { if c.F != nil && c.Vm == nil { - f.deleteCalcChain(f.getSheetID(sheet), c.R) + sheetID := f.getSheetID(sheet) + f.deleteCalcChain(sheetID, c.R) if c.F.T == STCellFormulaTypeShared && c.F.Ref != "" { si := c.F.Si for r, row := range ws.SheetData.Row { for col, cell := range row.C { if cell.F != nil && cell.F.Si != nil && *cell.F.Si == *si { ws.SheetData.Row[r].C[col].F = nil + f.deleteCalcChain(sheetID, cell.R) } } } diff --git a/sheetpr.go b/sheetpr.go index b0f3945121..73a76a967c 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -11,6 +11,8 @@ package excelize +import "reflect" + // SetPageMargins provides a function to set worksheet page margins. func (f *File) SetPageMargins(sheet string, opts *PageLayoutMarginsOptions) error { ws, err := f.workSheetReader(sheet) @@ -30,29 +32,13 @@ func (f *File) SetPageMargins(sheet string, opts *PageLayoutMarginsOptions) erro ws.PrintOptions = new(xlsxPrintOptions) } } - if opts.Bottom != nil { - preparePageMargins(ws) - ws.PageMargins.Bottom = *opts.Bottom - } - if opts.Footer != nil { - preparePageMargins(ws) - ws.PageMargins.Footer = *opts.Footer - } - if opts.Header != nil { - preparePageMargins(ws) - ws.PageMargins.Header = *opts.Header - } - if opts.Left != nil { - preparePageMargins(ws) - ws.PageMargins.Left = *opts.Left - } - if opts.Right != nil { - preparePageMargins(ws) - ws.PageMargins.Right = *opts.Right - } - if opts.Top != nil { - preparePageMargins(ws) - ws.PageMargins.Top = *opts.Top + s := reflect.ValueOf(opts).Elem() + for i := 0; i < 6; i++ { + if !s.Field(i).IsNil() { + preparePageMargins(ws) + name := s.Type().Field(i).Name + reflect.ValueOf(ws.PageMargins).Elem().FieldByName(name).Set(s.Field(i).Elem()) + } } if opts.Horizontally != nil { preparePrintOptions(ws) @@ -154,21 +140,13 @@ func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) { ws.SheetPr.PageSetUpPr.FitToPage = *opts.FitToPage } ws.setSheetOutlineProps(opts) - if opts.TabColorIndexed != nil { - prepareTabColor(ws) - ws.SheetPr.TabColor.Indexed = *opts.TabColorIndexed - } - if opts.TabColorRGB != nil { - prepareTabColor(ws) - ws.SheetPr.TabColor.RGB = *opts.TabColorRGB - } - if opts.TabColorTheme != nil { - prepareTabColor(ws) - ws.SheetPr.TabColor.Theme = *opts.TabColorTheme - } - if opts.TabColorTint != nil { - prepareTabColor(ws) - ws.SheetPr.TabColor.Tint = *opts.TabColorTint + s := reflect.ValueOf(opts).Elem() + for i := 5; i < 9; i++ { + if !s.Field(i).IsNil() { + prepareTabColor(ws) + name := s.Type().Field(i).Name + reflect.ValueOf(ws.SheetPr.TabColor).Elem().FieldByName(name[8:]).Set(s.Field(i).Elem()) + } } } From 58b5dae5eb4948a3cde238ced1ae05db159749f5 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 11 Nov 2022 01:50:07 +0800 Subject: [PATCH 128/213] Support update column style when inserting or deleting columns - Go Modules dependencies upgrade - Unify internal variable name - Unit test updated --- adjust.go | 46 +++++++++++++++++++++++++++++- adjust_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++ col.go | 36 ++++++++++-------------- go.mod | 6 ++-- go.sum | 18 ++++++------ sheetpr.go | 26 ++++------------- 6 files changed, 152 insertions(+), 56 deletions(-) diff --git a/adjust.go b/adjust.go index 65e82fca03..bf899274b2 100644 --- a/adjust.go +++ b/adjust.go @@ -70,6 +70,50 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) return nil } +// adjustCols provides a function to update column style when inserting or +// deleting columns. +func (f *File) adjustCols(ws *xlsxWorksheet, col, offset int) error { + if ws.Cols == nil { + return nil + } + for i := 0; i < len(ws.Cols.Col); i++ { + if offset > 0 { + if ws.Cols.Col[i].Max+1 == col { + ws.Cols.Col[i].Max += offset + continue + } + if ws.Cols.Col[i].Min >= col { + ws.Cols.Col[i].Min += offset + ws.Cols.Col[i].Max += offset + continue + } + if ws.Cols.Col[i].Min < col && ws.Cols.Col[i].Max >= col { + ws.Cols.Col[i].Max += offset + } + } + if offset < 0 { + if ws.Cols.Col[i].Min == col && ws.Cols.Col[i].Max == col { + if len(ws.Cols.Col) > 1 { + ws.Cols.Col = append(ws.Cols.Col[:i], ws.Cols.Col[i+1:]...) + } else { + ws.Cols.Col = nil + } + i-- + continue + } + if ws.Cols.Col[i].Min > col { + ws.Cols.Col[i].Min += offset + ws.Cols.Col[i].Max += offset + continue + } + if ws.Cols.Col[i].Min <= col && ws.Cols.Col[i].Max >= col { + ws.Cols.Col[i].Max += offset + } + } + } + return nil +} + // adjustColDimensions provides a function to update column dimensions when // inserting or deleting rows or columns. func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) error { @@ -91,7 +135,7 @@ func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) error { } } } - return nil + return f.adjustCols(ws, col, offset) } // adjustRowDimensions provides a function to update row dimensions when diff --git a/adjust_test.go b/adjust_test.go index 010955c60f..0325616e0d 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -366,3 +366,79 @@ func TestAdjustCalcChain(t *testing.T) { f.CalcChain = nil assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) } + +func TestAdjustCols(t *testing.T) { + sheetName := "Sheet1" + preset := func() (*File, error) { + f := NewFile() + if err := f.SetColWidth(sheetName, "J", "T", 5); err != nil { + return f, err + } + if err := f.SetSheetRow(sheetName, "J1", &[]string{"J1", "K1", "L1", "M1", "N1", "O1", "P1", "Q1", "R1", "S1", "T1"}); err != nil { + return f, err + } + return f, nil + } + baseTbl := []string{"B", "J", "O", "O", "O", "U", "V"} + insertTbl := []int{2, 2, 2, 5, 6, 2, 2} + expectedTbl := []map[string]float64{ + {"J": defaultColWidth, "K": defaultColWidth, "U": 5, "V": 5, "W": defaultColWidth}, + {"J": defaultColWidth, "K": defaultColWidth, "U": 5, "V": 5, "W": defaultColWidth}, + {"O": 5, "P": 5, "U": 5, "V": 5, "W": defaultColWidth}, + {"O": 5, "S": 5, "X": 5, "Y": 5, "Z": defaultColWidth}, + {"O": 5, "S": 5, "Y": 5, "X": 5, "AA": defaultColWidth}, + {"U": 5, "V": 5, "W": defaultColWidth}, + {"U": defaultColWidth, "V": defaultColWidth, "W": defaultColWidth}, + } + for idx, columnName := range baseTbl { + f, err := preset() + assert.NoError(t, err) + assert.NoError(t, f.InsertCols(sheetName, columnName, insertTbl[idx])) + for column, expected := range expectedTbl[idx] { + width, err := f.GetColWidth(sheetName, column) + assert.NoError(t, err) + assert.Equal(t, expected, width, column) + } + assert.NoError(t, f.Close()) + } + + baseTbl = []string{"B", "J", "O", "T"} + expectedTbl = []map[string]float64{ + {"H": defaultColWidth, "I": 5, "S": 5, "T": defaultColWidth}, + {"I": defaultColWidth, "J": 5, "S": 5, "T": defaultColWidth}, + {"I": defaultColWidth, "O": 5, "S": 5, "T": defaultColWidth}, + {"R": 5, "S": 5, "T": defaultColWidth, "U": defaultColWidth}, + } + for idx, columnName := range baseTbl { + f, err := preset() + assert.NoError(t, err) + assert.NoError(t, f.RemoveCol(sheetName, columnName)) + for column, expected := range expectedTbl[idx] { + width, err := f.GetColWidth(sheetName, column) + assert.NoError(t, err) + assert.Equal(t, expected, width, column) + } + assert.NoError(t, f.Close()) + } + + f, err := preset() + assert.NoError(t, err) + assert.NoError(t, f.SetColWidth(sheetName, "I", "I", 8)) + for i := 0; i <= 12; i++ { + assert.NoError(t, f.RemoveCol(sheetName, "I")) + } + for c := 9; c <= 21; c++ { + columnName, err := ColumnNumberToName(c) + assert.NoError(t, err) + width, err := f.GetColWidth(sheetName, columnName) + assert.NoError(t, err) + assert.Equal(t, defaultColWidth, width, columnName) + } + + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).Cols = nil + assert.NoError(t, f.RemoveCol(sheetName, "A")) + + assert.NoError(t, f.Close()) +} diff --git a/col.go b/col.go index 964cb9751f..846679701d 100644 --- a/col.go +++ b/col.go @@ -279,7 +279,7 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { // // err := f.SetColVisible("Sheet1", "D:F", false) func (f *File) SetColVisible(sheet, columns string, visible bool) error { - start, end, err := f.parseColRange(columns) + min, max, err := f.parseColRange(columns) if err != nil { return err } @@ -290,8 +290,8 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error { ws.Lock() defer ws.Unlock() colData := xlsxCol{ - Min: start, - Max: end, + Min: min, + Max: max, Width: defaultColWidth, // default width Hidden: !visible, CustomWidth: true, @@ -343,20 +343,20 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { } // parseColRange parse and convert column range with column name to the column number. -func (f *File) parseColRange(columns string) (start, end int, err error) { +func (f *File) parseColRange(columns string) (min, max int, err error) { colsTab := strings.Split(columns, ":") - start, err = ColumnNameToNumber(colsTab[0]) + min, err = ColumnNameToNumber(colsTab[0]) if err != nil { return } - end = start + max = min if len(colsTab) == 2 { - if end, err = ColumnNameToNumber(colsTab[1]); err != nil { + if max, err = ColumnNameToNumber(colsTab[1]); err != nil { return } } - if end < start { - start, end = end, start + if max < min { + min, max = max, min } return } @@ -416,7 +416,7 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { // // err = f.SetColStyle("Sheet1", "C:F", style) func (f *File) SetColStyle(sheet, columns string, styleID int) error { - start, end, err := f.parseColRange(columns) + min, max, err := f.parseColRange(columns) if err != nil { return err } @@ -436,8 +436,8 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { ws.Cols = &xlsxCols{} } ws.Cols.Col = flatCols(xlsxCol{ - Min: start, - Max: end, + Min: min, + Max: max, Width: defaultColWidth, Style: styleID, }, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol { @@ -452,7 +452,7 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { }) ws.Unlock() if rows := len(ws.SheetData.Row); rows > 0 { - for col := start; col <= end; col++ { + for col := min; col <= max; col++ { from, _ := CoordinatesToCellName(col, 1) to, _ := CoordinatesToCellName(col, rows) err = f.SetCellStyle(sheet, from, to, styleID) @@ -467,21 +467,13 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { // f := excelize.NewFile() // err := f.SetColWidth("Sheet1", "A", "H", 20) func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error { - min, err := ColumnNameToNumber(startCol) - if err != nil { - return err - } - max, err := ColumnNameToNumber(endCol) + min, max, err := f.parseColRange(startCol + ":" + endCol) if err != nil { return err } if width > MaxColumnWidth { return ErrColumnWidth } - if min > max { - min, max = max, min - } - ws, err := f.workSheetReader(sheet) if err != nil { return err diff --git a/go.mod b/go.mod index 644a6aaad5..e3ed4bb9f5 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/stretchr/testify v1.8.0 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-20221012134737-56aed061732a + golang.org/x/crypto v0.2.0 golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 - golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 - golang.org/x/text v0.3.8 + golang.org/x/net v0.2.0 + golang.org/x/text v0.4.0 ) require github.com/richardlehane/msoleps v1.0.3 // indirect diff --git a/go.sum b/go.sum index 69d81ecaa8..6b97c1f2f1 100644 --- a/go.sum +++ b/go.sum @@ -22,34 +22,32 @@ github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg= -golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= +golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 h1:MgJ6t2zo8v0tbmLCueaCbF1RM+TtB0rs3Lv8DGtOIpY= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/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/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/sheetpr.go b/sheetpr.go index 73a76a967c..41ca08291b 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -163,26 +163,12 @@ func (f *File) SetSheetProps(sheet string, opts *SheetPropsOptions) error { if ws.SheetFormatPr == nil { ws.SheetFormatPr = &xlsxSheetFormatPr{DefaultRowHeight: defaultRowHeight} } - if opts.BaseColWidth != nil { - ws.SheetFormatPr.BaseColWidth = *opts.BaseColWidth - } - if opts.DefaultColWidth != nil { - ws.SheetFormatPr.DefaultColWidth = *opts.DefaultColWidth - } - if opts.DefaultRowHeight != nil { - ws.SheetFormatPr.DefaultRowHeight = *opts.DefaultRowHeight - } - if opts.CustomHeight != nil { - ws.SheetFormatPr.CustomHeight = *opts.CustomHeight - } - if opts.ZeroHeight != nil { - ws.SheetFormatPr.ZeroHeight = *opts.ZeroHeight - } - if opts.ThickTop != nil { - ws.SheetFormatPr.ThickTop = *opts.ThickTop - } - if opts.ThickBottom != nil { - ws.SheetFormatPr.ThickBottom = *opts.ThickBottom + s := reflect.ValueOf(opts).Elem() + for i := 11; i < 18; i++ { + if !s.Field(i).IsNil() { + name := s.Type().Field(i).Name + reflect.ValueOf(ws.SheetFormatPr).Elem().FieldByName(name).Set(s.Field(i).Elem()) + } } return err } From bd5dd17673f767b9f4643423c77eec486f2ad53f Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 12 Nov 2022 00:02:11 +0800 Subject: [PATCH 129/213] This is a breaking change, remove partial internal error log print, throw XML deserialize error - Add error return value for the `GetComments`, `GetDefaultFont` and `SetDefaultFont` functions - Update unit tests --- adjust_test.go | 16 +++---- calcchain.go | 20 ++++---- calcchain_test.go | 26 +++++++++-- cell.go | 92 +++++++++++++++++++++---------------- cell_test.go | 113 ++++++++++++++++++++++++++++++++++------------ chart_test.go | 18 ++++++++ col.go | 78 ++++++++++++++++++-------------- col_test.go | 13 ++++++ comment.go | 102 +++++++++++++++++++++++------------------ comment_test.go | 48 +++++++++++++++++--- docProps.go | 4 -- docProps_test.go | 12 ++--- drawing.go | 25 ++++++---- drawing_test.go | 17 ++++--- errors.go | 5 -- excelize.go | 9 ++-- excelize_test.go | 30 +++++++++++- file.go | 4 +- merge_test.go | 6 +++ picture.go | 11 +++-- picture_test.go | 16 +++++-- rows.go | 39 +++++++++------- rows_test.go | 17 ++++++- shape.go | 14 ++++-- shape_test.go | 11 +++++ sheet.go | 8 ++-- sheet_test.go | 6 +++ stream_test.go | 4 +- styles.go | 112 ++++++++++++++++++++++++++++++--------------- styles_test.go | 76 +++++++++++++++++++++++++------ 30 files changed, 655 insertions(+), 297 deletions(-) diff --git a/adjust_test.go b/adjust_test.go index 0325616e0d..3ce1796ed3 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -10,7 +10,7 @@ import ( func TestAdjustMergeCells(t *testing.T) { f := NewFile() - // testing adjustAutoFilter with illegal cell reference. + // Test adjustAutoFilter with illegal cell reference. assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -57,7 +57,7 @@ func TestAdjustMergeCells(t *testing.T) { }, }, columns, 1, -1)) - // testing adjustMergeCells + // Test adjustMergeCells. var cases []struct { label string ws *xlsxWorksheet @@ -68,7 +68,7 @@ func TestAdjustMergeCells(t *testing.T) { expectRect []int } - // testing insert + // Test insert. cases = []struct { label string ws *xlsxWorksheet @@ -139,7 +139,7 @@ func TestAdjustMergeCells(t *testing.T) { assert.Equal(t, c.expectRect, c.ws.MergeCells.Cells[0].rect, c.label) } - // testing delete + // Test delete, cases = []struct { label string ws *xlsxWorksheet @@ -227,7 +227,7 @@ func TestAdjustMergeCells(t *testing.T) { assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label) } - // testing delete one row/column + // Test delete one row or column cases = []struct { label string ws *xlsxWorksheet @@ -324,13 +324,13 @@ func TestAdjustTable(t *testing.T) { f = NewFile() assert.NoError(t, f.AddTable(sheetName, "A1", "D5", "")) - // Test adjust table with non-table part + // Test adjust table with non-table part. f.Pkg.Delete("xl/tables/table1.xml") assert.NoError(t, f.RemoveRow(sheetName, 1)) - // Test adjust table with unsupported charset + // Test adjust table with unsupported charset. f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) assert.NoError(t, f.RemoveRow(sheetName, 1)) - // Test adjust table with invalid table range reference + // Test adjust table with invalid table range reference. f.Pkg.Store("xl/tables/table1.xml", []byte(`
`)) assert.NoError(t, f.RemoveRow(sheetName, 1)) } diff --git a/calcchain.go b/calcchain.go index 80928c24a7..3aa5d812f3 100644 --- a/calcchain.go +++ b/calcchain.go @@ -15,23 +15,19 @@ import ( "bytes" "encoding/xml" "io" - "log" ) // calcChainReader provides a function to get the pointer to the structure // after deserialization of xl/calcChain.xml. -func (f *File) calcChainReader() *xlsxCalcChain { - var err error - +func (f *File) calcChainReader() (*xlsxCalcChain, error) { if f.CalcChain == nil { f.CalcChain = new(xlsxCalcChain) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathCalcChain)))). + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathCalcChain)))). Decode(f.CalcChain); err != nil && err != io.EOF { - log.Printf("xml decode error: %s", err) + return f.CalcChain, err } } - - return f.CalcChain + return f.CalcChain, nil } // calcChainWriter provides a function to save xl/calcChain.xml after @@ -45,8 +41,11 @@ func (f *File) calcChainWriter() { // deleteCalcChain provides a function to remove cell reference on the // calculation chain. -func (f *File) deleteCalcChain(index int, cell string) { - calc := f.calcChainReader() +func (f *File) deleteCalcChain(index int, cell string) error { + calc, err := f.calcChainReader() + if err != nil { + return err + } if calc != nil { calc.C = xlsxCalcChainCollection(calc.C).Filter(func(c xlsxCalcChainC) bool { return !((c.I == index && c.R == cell) || (c.I == index && cell == "") || (c.I == 0 && c.R == cell)) @@ -64,6 +63,7 @@ func (f *File) deleteCalcChain(index int, cell string) { } } } + return err } type xlsxCalcChainCollection []xlsxCalcChainC diff --git a/calcchain_test.go b/calcchain_test.go index c36655bc5e..fae3a5130b 100644 --- a/calcchain_test.go +++ b/calcchain_test.go @@ -1,12 +1,18 @@ package excelize -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestCalcChainReader(t *testing.T) { f := NewFile() + // Test read calculation chain with unsupported charset. f.CalcChain = nil f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset) - f.calcChainReader() + _, err := f.calcChainReader() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestDeleteCalcChain(t *testing.T) { @@ -15,5 +21,19 @@ func TestDeleteCalcChain(t *testing.T) { f.ContentTypes.Overrides = append(f.ContentTypes.Overrides, xlsxOverride{ PartName: "/xl/calcChain.xml", }) - f.deleteCalcChain(1, "A1") + assert.NoError(t, f.deleteCalcChain(1, "A1")) + + f.CalcChain = nil + f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteCalcChain(1, "A1"), "XML syntax error on line 1: invalid UTF-8") + + f.CalcChain = nil + f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetCellFormula("Sheet1", "A1", ""), "XML syntax error on line 1: invalid UTF-8") + + formulaType, ref := STCellFormulaTypeShared, "C1:C5" + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType})) + f.CalcChain = nil + f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetCellValue("Sheet1", "C1", true), "XML syntax error on line 1: invalid UTF-8") } diff --git a/cell.go b/cell.go index c8fa9b2516..cbb7932a93 100644 --- a/cell.go +++ b/cell.go @@ -66,7 +66,11 @@ var cellTypes = map[string]CellType{ // values will be the same in a merged range. func (f *File) GetCellValue(sheet, cell string, opts ...Options) (string, error) { return f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { - val, err := c.getValueFrom(f, f.sharedStringsReader(), parseOptions(opts...).RawCellValue) + sst, err := f.sharedStringsReader() + if err != nil { + return "", true, err + } + val, err := c.getValueFrom(f, sst, parseOptions(opts...).RawCellValue) return val, true, err }) } @@ -173,23 +177,26 @@ func (c *xlsxC) hasValue() bool { } // removeFormula delete formula for the cell. -func (f *File) removeFormula(c *xlsxC, ws *xlsxWorksheet, sheet string) { +func (f *File) removeFormula(c *xlsxC, ws *xlsxWorksheet, sheet string) error { if c.F != nil && c.Vm == nil { sheetID := f.getSheetID(sheet) - f.deleteCalcChain(sheetID, c.R) + if err := f.deleteCalcChain(sheetID, c.R); err != nil { + return err + } if c.F.T == STCellFormulaTypeShared && c.F.Ref != "" { si := c.F.Si for r, row := range ws.SheetData.Row { for col, cell := range row.C { if cell.F != nil && cell.F.Si != nil && *cell.F.Si == *si { ws.SheetData.Row[r].C[col].F = nil - f.deleteCalcChain(sheetID, cell.R) + _ = f.deleteCalcChain(sheetID, cell.R) } } } } c.F = nil } + return nil } // setCellIntFunc is a wrapper of SetCellInt. @@ -289,8 +296,7 @@ func (f *File) SetCellInt(sheet, cell string, value int) error { c.S = f.prepareCellStyle(ws, col, row, c.S) c.T, c.V = setCellInt(value) c.IS = nil - f.removeFormula(c, ws, sheet) - return err + return f.removeFormula(c, ws, sheet) } // setCellInt prepares cell type and string type cell value by a given @@ -316,8 +322,7 @@ func (f *File) SetCellBool(sheet, cell string, value bool) error { c.S = f.prepareCellStyle(ws, col, row, c.S) c.T, c.V = setCellBool(value) c.IS = nil - f.removeFormula(c, ws, sheet) - return err + return f.removeFormula(c, ws, sheet) } // setCellBool prepares cell type and string type cell value by a given @@ -354,8 +359,7 @@ func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSiz c.S = f.prepareCellStyle(ws, col, row, c.S) c.T, c.V = setCellFloat(value, precision, bitSize) c.IS = nil - f.removeFormula(c, ws, sheet) - return err + return f.removeFormula(c, ws, sheet) } // setCellFloat prepares cell type and string type cell value by a given @@ -379,10 +383,11 @@ func (f *File) SetCellStr(sheet, cell, value string) error { ws.Lock() defer ws.Unlock() c.S = f.prepareCellStyle(ws, col, row, c.S) - c.T, c.V, err = f.setCellString(value) + if c.T, c.V, err = f.setCellString(value); err != nil { + return err + } c.IS = nil - f.removeFormula(c, ws, sheet) - return err + return f.removeFormula(c, ws, sheet) } // setCellString provides a function to set string type to shared string @@ -429,7 +434,10 @@ func (f *File) setSharedString(val string) (int, error) { if err := f.sharedStringsLoader(); err != nil { return 0, err } - sst := f.sharedStringsReader() + sst, err := f.sharedStringsReader() + if err != nil { + return 0, err + } f.Lock() defer f.Unlock() if i, ok := f.sharedStringsMap[val]; ok { @@ -498,7 +506,7 @@ func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) { return "FALSE", nil } } - return f.formattedValue(c.S, c.V, raw), nil + return f.formattedValue(c.S, c.V, raw) } // setCellDefault prepares cell type and string type cell value by a given @@ -529,7 +537,7 @@ func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) { c.V = strconv.FormatFloat(excelTime, 'G', 15, 64) } } - return f.formattedValue(c.S, c.V, raw), nil + return f.formattedValue(c.S, c.V, raw) } // getValueFrom return a value from a column/row cell, this function is @@ -548,18 +556,18 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { xlsxSI := 0 xlsxSI, _ = strconv.Atoi(c.V) if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { - return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw), nil + return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw) } if len(d.SI) > xlsxSI { - return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw), nil + return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw) } } - return f.formattedValue(c.S, c.V, raw), nil + return f.formattedValue(c.S, c.V, raw) case "inlineStr": if c.IS != nil { - return f.formattedValue(c.S, c.IS.String(), raw), nil + return f.formattedValue(c.S, c.IS.String(), raw) } - return f.formattedValue(c.S, c.V, raw), nil + return f.formattedValue(c.S, c.V, raw) default: if isNum, precision, decimal := isNumeric(c.V); isNum && !raw { if precision > 15 { @@ -568,7 +576,7 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { c.V = strconv.FormatFloat(decimal, 'f', -1, 64) } } - return f.formattedValue(c.S, c.V, raw), nil + return f.formattedValue(c.S, c.V, raw) } } @@ -587,8 +595,7 @@ func (f *File) SetCellDefault(sheet, cell, value string) error { defer ws.Unlock() c.S = f.prepareCellStyle(ws, col, row, c.S) c.setCellDefault(value) - f.removeFormula(c, ws, sheet) - return err + return f.removeFormula(c, ws, sheet) } // GetCellFormula provides a function to get formula from cell by given @@ -698,8 +705,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) } if formula == "" { c.F = nil - f.deleteCalcChain(f.getSheetID(sheet), cell) - return err + return f.deleteCalcChain(f.getSheetID(sheet), cell) } if c.F != nil { @@ -926,7 +932,10 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro if err != nil || c.T != "s" { return } - sst := f.sharedStringsReader() + sst, err := f.sharedStringsReader() + if err != nil { + return + } if len(sst.SI) <= siIdx || siIdx < 0 { return } @@ -1145,7 +1154,11 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { return err } c.S = f.prepareCellStyle(ws, col, row, c.S) - si, sst := xlsxSI{}, f.sharedStringsReader() + si := xlsxSI{} + sst, err := f.sharedStringsReader() + if err != nil { + return err + } if si.R, err = setRichText(runs); err != nil { return err } @@ -1286,19 +1299,22 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c // formattedValue provides a function to returns a value after formatted. If // it is possible to apply a format to the cell value, it will do so, if not // then an error will be returned, along with the raw value of the cell. -func (f *File) formattedValue(s int, v string, raw bool) string { +func (f *File) formattedValue(s int, v string, raw bool) (string, error) { if raw { - return v + return v, nil } if s == 0 { - return v + return v, nil + } + styleSheet, err := f.stylesReader() + if err != nil { + return v, err } - styleSheet := f.stylesReader() if styleSheet.CellXfs == nil { - return v + return v, err } if s >= len(styleSheet.CellXfs.Xf) { - return v + return v, err } var numFmtID int if styleSheet.CellXfs.Xf[s].NumFmtID != nil { @@ -1309,17 +1325,17 @@ func (f *File) formattedValue(s int, v string, raw bool) string { date1904 = wb.WorkbookPr.Date1904 } if ok := builtInNumFmtFunc[numFmtID]; ok != nil { - return ok(v, builtInNumFmt[numFmtID], date1904) + return ok(v, builtInNumFmt[numFmtID], date1904), err } if styleSheet.NumFmts == nil { - return v + return v, err } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { - return format(v, xlsxFmt.FormatCode, date1904) + return format(v, xlsxFmt.FormatCode, date1904), err } } - return v + return v, err } // prepareCellStyle provides a function to prepare style index of cell in diff --git a/cell_test.go b/cell_test.go index 6689c36aff..18bc10113c 100644 --- a/cell_test.go +++ b/cell_test.go @@ -188,6 +188,11 @@ func TestSetCellValue(t *testing.T) { B2, err := f.GetCellValue("Sheet1", "B2") assert.NoError(t, err) assert.Equal(t, "0.50", B2) + + // Test set cell value with unsupported charset shared strings table + f.SharedStrings = nil + f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetCellValue("Sheet1", "A1", "A1"), "XML syntax error on line 1: invalid UTF-8") } func TestSetCellValues(t *testing.T) { @@ -199,7 +204,7 @@ func TestSetCellValues(t *testing.T) { assert.NoError(t, err) assert.Equal(t, v, "12/31/10 00:00") - // test date value lower than min date supported by Excel + // Test date value lower than min date supported by Excel err = f.SetCellValue("Sheet1", "A1", time.Date(1600, time.December, 31, 0, 0, 0, 0, time.UTC)) assert.NoError(t, err) @@ -377,6 +382,12 @@ func TestGetCellValue(t *testing.T) { "2020-07-10 15:00:00.000", }, rows[0]) assert.NoError(t, err) + + // Test get cell value with unsupported charset shared strings table. + f.SharedStrings = nil + f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) + _, value := f.GetCellValue("Sheet1", "A1") + assert.EqualError(t, value, "XML syntax error on line 1: invalid UTF-8") } func TestGetCellType(t *testing.T) { @@ -395,7 +406,9 @@ func TestGetCellType(t *testing.T) { func TestGetValueFrom(t *testing.T) { f := NewFile() c := xlsxC{T: "s"} - value, err := c.getValueFrom(f, f.sharedStringsReader(), false) + sst, err := f.sharedStringsReader() + assert.NoError(t, err) + value, err := c.getValueFrom(f, sst, false) assert.NoError(t, err) assert.Equal(t, "", value) } @@ -566,36 +579,46 @@ func TestGetCellRichText(t *testing.T) { runsSource[1].Font.Color = strings.ToUpper(runsSource[1].Font.Color) assert.True(t, reflect.DeepEqual(runsSource[1].Font, runs[1].Font), "should get the same font") - // Test get cell rich text when string item index overflow + // Test get cell rich text when string item index overflow. ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "2" runs, err = f.GetCellRichText("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, 0, len(runs)) - // Test get cell rich text when string item index is negative + // Test get cell rich text when string item index is negative. ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "-1" runs, err = f.GetCellRichText("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, 0, len(runs)) - // Test get cell rich text on invalid string item index + // Test get cell rich text on invalid string item index. ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "x" _, err = f.GetCellRichText("Sheet1", "A1") assert.EqualError(t, err, "strconv.Atoi: parsing \"x\": invalid syntax") - // Test set cell rich text on not exists worksheet + // Test set cell rich text on not exists worksheet. _, err = f.GetCellRichText("SheetN", "A1") assert.EqualError(t, err, "sheet SheetN does not exist") - // Test set cell rich text with illegal cell reference + // Test set cell rich text with illegal cell reference. _, err = f.GetCellRichText("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - // Test set rich text color theme without tint + // Test set rich text color theme without tint. assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTheme: &theme}}})) - // Test set rich text color tint without theme + // Test set rich text color tint without theme. assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTint: 0.5}}})) + + // Test set cell rich text with unsupported charset shared strings table. + f.SharedStrings = nil + f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", runsSource), "XML syntax error on line 1: invalid UTF-8") + // Test get cell rich text with unsupported charset shared strings table. + f.SharedStrings = nil + f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) + _, err = f.GetCellRichText("Sheet1", "A1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestSetCellRichText(t *testing.T) { @@ -689,80 +712,108 @@ func TestSetCellRichText(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellRichText.xlsx"))) - // Test set cell rich text on not exists worksheet + // Test set cell rich text on not exists worksheet. assert.EqualError(t, f.SetCellRichText("SheetN", "A1", richTextRun), "sheet SheetN does not exist") - // Test set cell rich text with illegal cell reference + // Test set cell rich text with illegal cell reference. assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) richTextRun = []RichTextRun{{Text: strings.Repeat("s", TotalCellChars+1)}} - // Test set cell rich text with characters over the maximum limit + // Test set cell rich text with characters over the maximum limit. assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", richTextRun), ErrCellCharsLength.Error()) } -func TestFormattedValue2(t *testing.T) { +func TestFormattedValue(t *testing.T) { f := NewFile() - assert.Equal(t, "43528", f.formattedValue(0, "43528", false)) + result, err := f.formattedValue(0, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "43528", result) - assert.Equal(t, "43528", f.formattedValue(15, "43528", false)) + result, err = f.formattedValue(15, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "43528", result) - assert.Equal(t, "43528", f.formattedValue(1, "43528", false)) + result, err = f.formattedValue(1, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "43528", result) customNumFmt := "[$-409]MM/DD/YYYY" - _, err := f.NewStyle(&Style{ + _, err = f.NewStyle(&Style{ CustomNumFmt: &customNumFmt, }) assert.NoError(t, err) - assert.Equal(t, "03/04/2019", f.formattedValue(1, "43528", false)) + result, err = f.formattedValue(1, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "03/04/2019", result) - // formatted value with no built-in number format ID + // Test format value with no built-in number format ID. numFmtID := 5 f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: &numFmtID, }) - assert.Equal(t, "43528", f.formattedValue(2, "43528", false)) + result, err = f.formattedValue(2, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "43528", result) - // formatted value with invalid number format ID + // Test format value with invalid number format ID. f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: nil, }) - assert.Equal(t, "43528", f.formattedValue(3, "43528", false)) + result, err = f.formattedValue(3, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "43528", result) - // formatted value with empty number format + // Test format value with empty number format. f.Styles.NumFmts = nil f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: &numFmtID, }) - assert.Equal(t, "43528", f.formattedValue(1, "43528", false)) + result, err = f.formattedValue(1, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "43528", result) - // formatted decimal value with build-in number format ID + // Test format decimal value with build-in number format ID. styleID, err := f.NewStyle(&Style{ NumFmt: 1, }) assert.NoError(t, err) - assert.Equal(t, "311", f.formattedValue(styleID, "310.56", false)) + result, err = f.formattedValue(styleID, "310.56", false) + assert.NoError(t, err) + assert.Equal(t, "311", result) for _, fn := range builtInNumFmtFunc { assert.Equal(t, "0_0", fn("0_0", "", false)) } + + // Test format value with unsupported charset style sheet. + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + _, err = f.formattedValue(1, "43528", false) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestFormattedValueNilXfs(t *testing.T) { // Set the CellXfs to nil and verify that the formattedValue function does not crash. f := NewFile() f.Styles.CellXfs = nil - assert.Equal(t, "43528", f.formattedValue(3, "43528", false)) + result, err := f.formattedValue(3, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "43528", result) } func TestFormattedValueNilNumFmts(t *testing.T) { // Set the NumFmts value to nil and verify that the formattedValue function does not crash. f := NewFile() f.Styles.NumFmts = nil - assert.Equal(t, "43528", f.formattedValue(3, "43528", false)) + result, err := f.formattedValue(3, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "43528", result) } func TestFormattedValueNilWorkbook(t *testing.T) { // Set the Workbook value to nil and verify that the formattedValue function does not crash. f := NewFile() f.WorkBook = nil - assert.Equal(t, "43528", f.formattedValue(3, "43528", false)) + result, err := f.formattedValue(3, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "43528", result) } func TestFormattedValueNilWorkbookPr(t *testing.T) { @@ -770,7 +821,9 @@ func TestFormattedValueNilWorkbookPr(t *testing.T) { // crash. f := NewFile() f.WorkBook.WorkbookPr = nil - assert.Equal(t, "43528", f.formattedValue(3, "43528", false)) + result, err := f.formattedValue(3, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "43528", result) } func TestSharedStringsError(t *testing.T) { diff --git a/chart_test.go b/chart_test.go index 6d40b44f93..dac724a3d2 100644 --- a/chart_test.go +++ b/chart_test.go @@ -95,6 +95,24 @@ func TestChartSize(t *testing.T) { func TestAddDrawingChart(t *testing.T) { f := NewFile() assert.EqualError(t, f.addDrawingChart("SheetN", "", "", 0, 0, 0, nil), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) + + path := "xl/drawings/drawing1.xml" + f.Pkg.Store(path, MacintoshCyrillicCharset) + assert.EqualError(t, f.addDrawingChart("Sheet1", path, "A1", 0, 0, 0, &pictureOptions{}), "XML syntax error on line 1: invalid UTF-8") +} + +func TestAddSheetDrawingChart(t *testing.T) { + f := NewFile() + path := "xl/drawings/drawing1.xml" + f.Pkg.Store(path, MacintoshCyrillicCharset) + assert.EqualError(t, f.addSheetDrawingChart(path, 0, &pictureOptions{}), "XML syntax error on line 1: invalid UTF-8") +} + +func TestDeleteDrawing(t *testing.T) { + f := NewFile() + path := "xl/drawings/drawing1.xml" + f.Pkg.Store(path, MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteDrawing(0, 0, path, "Chart"), "XML syntax error on line 1: invalid UTF-8") } func TestAddChart(t *testing.T) { diff --git a/col.go b/col.go index 846679701d..b93087738c 100644 --- a/col.go +++ b/col.go @@ -38,6 +38,7 @@ type Cols struct { sheet string f *File sheetXML []byte + sst *xlsxSST } // GetCols gets the value of all cells by columns on the worksheet based on the @@ -87,17 +88,14 @@ func (cols *Cols) Error() error { // Rows return the current column's row values. func (cols *Cols) Rows(opts ...Options) ([]string, error) { - var ( - err error - inElement string - cellCol, cellRow int - rows []string - ) + var rowIterator rowXMLIterator if cols.stashCol >= cols.curCol { - return rows, err + return rowIterator.cells, rowIterator.err } cols.rawCellValue = parseOptions(opts...).RawCellValue - d := cols.f.sharedStringsReader() + if cols.sst, rowIterator.err = cols.f.sharedStringsReader(); rowIterator.err != nil { + return rowIterator.cells, rowIterator.err + } decoder := cols.f.xmlNewDecoder(bytes.NewReader(cols.sheetXML)) for { token, _ := decoder.Token() @@ -106,42 +104,25 @@ func (cols *Cols) Rows(opts ...Options) ([]string, error) { } switch xmlElement := token.(type) { case xml.StartElement: - inElement = xmlElement.Name.Local - if inElement == "row" { - cellCol = 0 - cellRow++ + rowIterator.inElement = xmlElement.Name.Local + if rowIterator.inElement == "row" { + rowIterator.cellCol = 0 + rowIterator.cellRow++ attrR, _ := attrValToInt("r", xmlElement.Attr) if attrR != 0 { - cellRow = attrR + rowIterator.cellRow = attrR } } - if inElement == "c" { - cellCol++ - for _, attr := range xmlElement.Attr { - if attr.Name.Local == "r" { - if cellCol, cellRow, err = CellNameToCoordinates(attr.Value); err != nil { - return rows, err - } - } - } - blank := cellRow - len(rows) - for i := 1; i < blank; i++ { - rows = append(rows, "") - } - if cellCol == cols.curCol { - colCell := xlsxC{} - _ = decoder.DecodeElement(&colCell, &xmlElement) - val, _ := colCell.getValueFrom(cols.f, d, cols.rawCellValue) - rows = append(rows, val) - } + if cols.rowXMLHandler(&rowIterator, &xmlElement, decoder); rowIterator.err != nil { + return rowIterator.cells, rowIterator.err } case xml.EndElement: if xmlElement.Name.Local == "sheetData" { - return rows, err + return rowIterator.cells, rowIterator.err } } } - return rows, err + return rowIterator.cells, rowIterator.err } // columnXMLIterator defined runtime use field for the worksheet column SAX parser. @@ -183,6 +164,30 @@ func columnXMLHandler(colIterator *columnXMLIterator, xmlElement *xml.StartEleme } } +// rowXMLHandler parse the row XML element of the worksheet. +func (cols *Cols) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement, decoder *xml.Decoder) { + if rowIterator.inElement == "c" { + rowIterator.cellCol++ + for _, attr := range xmlElement.Attr { + if attr.Name.Local == "r" { + if rowIterator.cellCol, rowIterator.cellRow, rowIterator.err = CellNameToCoordinates(attr.Value); rowIterator.err != nil { + return + } + } + } + blank := rowIterator.cellRow - len(rowIterator.cells) + for i := 1; i < blank; i++ { + rowIterator.cells = append(rowIterator.cells, "") + } + if rowIterator.cellCol == cols.curCol { + colCell := xlsxC{} + _ = decoder.DecodeElement(&colCell, xmlElement) + val, _ := colCell.getValueFrom(cols.f, cols.sst, cols.rawCellValue) + rowIterator.cells = append(rowIterator.cells, val) + } + } +} + // Cols returns a columns iterator, used for streaming reading data for a // worksheet with a large data. This function is concurrency safe. For // example: @@ -420,7 +425,10 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { if err != nil { return err } - s := f.stylesReader() + s, err := f.stylesReader() + if err != nil { + return err + } s.Lock() if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { s.Unlock() diff --git a/col_test.go b/col_test.go index f786335709..0ed1906166 100644 --- a/col_test.go +++ b/col_test.go @@ -56,6 +56,15 @@ func TestCols(t *testing.T) { }) _, err = f.Rows("Sheet1") assert.NoError(t, err) + + // Test columns iterator with unsupported charset shared strings table. + f.SharedStrings = nil + f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) + cols, err = f.Cols("Sheet1") + assert.NoError(t, err) + cols.Next() + _, err = cols.Rows() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestColumnsIterator(t *testing.T) { @@ -316,6 +325,10 @@ func TestSetColStyle(t *testing.T) { assert.NoError(t, err) assert.Equal(t, styleID, cellStyleID) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetColStyle.xlsx"))) + // Test set column style with unsupported charset style sheet. + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetColStyle("Sheet1", "C:F", styleID), "XML syntax error on line 1: invalid UTF-8") } func TestColWidth(t *testing.T) { diff --git a/comment.go b/comment.go index 28c6cf82fc..eec5fa63da 100644 --- a/comment.go +++ b/comment.go @@ -16,7 +16,6 @@ import ( "encoding/xml" "fmt" "io" - "log" "path/filepath" "strconv" "strings" @@ -24,8 +23,8 @@ import ( // GetComments retrieves all comments and returns a map of worksheet name to // the worksheet comments. -func (f *File) GetComments() (comments map[string][]Comment) { - comments = map[string][]Comment{} +func (f *File) GetComments() (map[string][]Comment, error) { + comments := map[string][]Comment{} for n, path := range f.sheetMap { target := f.getSheetComments(filepath.Base(path)) if target == "" { @@ -34,12 +33,16 @@ func (f *File) GetComments() (comments map[string][]Comment) { if !strings.HasPrefix(target, "/") { target = "xl" + strings.TrimPrefix(target, "..") } - if d := f.commentsReader(strings.TrimPrefix(target, "/")); d != nil { + cmts, err := f.commentsReader(strings.TrimPrefix(target, "/")) + if err != nil { + return comments, err + } + if cmts != nil { var sheetComments []Comment - for _, comment := range d.CommentList.Comment { + for _, comment := range cmts.CommentList.Comment { sheetComment := Comment{} - if comment.AuthorID < len(d.Authors.Author) { - sheetComment.Author = d.Authors.Author[comment.AuthorID] + if comment.AuthorID < len(cmts.Authors.Author) { + sheetComment.Author = cmts.Authors.Author[comment.AuthorID] } sheetComment.Cell = comment.Ref sheetComment.AuthorID = comment.AuthorID @@ -60,7 +63,7 @@ func (f *File) GetComments() (comments map[string][]Comment) { comments[n] = sheetComments } } - return + return comments, nil } // getSheetComments provides the method to get the target comment reference by @@ -129,7 +132,9 @@ func (f *File) AddComment(sheet string, comment Comment) error { if err = f.addDrawingVML(commentID, drawingVML, comment.Cell, rows+1, cols); err != nil { return err } - f.addComment(commentsXML, comment) + if err = f.addComment(commentsXML, comment); err != nil { + return err + } f.addContentTypePart(commentID, "comments") return err } @@ -139,34 +144,36 @@ func (f *File) AddComment(sheet string, comment Comment) error { // // err := f.DeleteComment("Sheet1", "A30") func (f *File) DeleteComment(sheet, cell string) error { - var err error sheetXMLPath, ok := f.getSheetXMLPath(sheet) if !ok { - err = newNoExistSheetError(sheet) - return err + return newNoExistSheetError(sheet) } commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath)) if !strings.HasPrefix(commentsXML, "/") { commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..") } commentsXML = strings.TrimPrefix(commentsXML, "/") - if comments := f.commentsReader(commentsXML); comments != nil { - for i := 0; i < len(comments.CommentList.Comment); i++ { - cmt := comments.CommentList.Comment[i] + cmts, err := f.commentsReader(commentsXML) + if err != nil { + return err + } + if cmts != nil { + for i := 0; i < len(cmts.CommentList.Comment); i++ { + cmt := cmts.CommentList.Comment[i] if cmt.Ref != cell { continue } - if len(comments.CommentList.Comment) > 1 { - comments.CommentList.Comment = append( - comments.CommentList.Comment[:i], - comments.CommentList.Comment[i+1:]..., + if len(cmts.CommentList.Comment) > 1 { + cmts.CommentList.Comment = append( + cmts.CommentList.Comment[:i], + cmts.CommentList.Comment[i+1:]..., ) i-- continue } - comments.CommentList.Comment = nil + cmts.CommentList.Comment = nil } - f.Comments[commentsXML] = comments + f.Comments[commentsXML] = cmts } return err } @@ -209,7 +216,10 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, }, } // load exist comment shapes from xl/drawings/vmlDrawing%d.vml - d := f.decodeVMLDrawingReader(drawingVML) + d, err := f.decodeVMLDrawingReader(drawingVML) + if err != nil { + return err + } if d != nil { for _, v := range d.Shape { s := xlsxShape{ @@ -274,22 +284,30 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, // addComment provides a function to create chart as xl/comments%d.xml by // given cell and format sets. -func (f *File) addComment(commentsXML string, comment Comment) { +func (f *File) addComment(commentsXML string, comment Comment) error { if comment.Author == "" { comment.Author = "Author" } if len(comment.Author) > MaxFieldLength { comment.Author = comment.Author[:MaxFieldLength] } - comments, authorID := f.commentsReader(commentsXML), 0 - if comments == nil { - comments = &xlsxComments{Authors: xlsxAuthor{Author: []string{comment.Author}}} + cmts, err := f.commentsReader(commentsXML) + if err != nil { + return err + } + var authorID int + if cmts == nil { + cmts = &xlsxComments{Authors: xlsxAuthor{Author: []string{comment.Author}}} + } + if inStrSlice(cmts.Authors.Author, comment.Author, true) == -1 { + cmts.Authors.Author = append(cmts.Authors.Author, comment.Author) + authorID = len(cmts.Authors.Author) - 1 } - if inStrSlice(comments.Authors.Author, comment.Author, true) == -1 { - comments.Authors.Author = append(comments.Authors.Author, comment.Author) - authorID = len(comments.Authors.Author) - 1 + defaultFont, err := f.GetDefaultFont() + if err != nil { + return err } - defaultFont, chars, cmt := f.GetDefaultFont(), 0, xlsxComment{ + chars, cmt := 0, xlsxComment{ Ref: comment.Cell, AuthorID: authorID, Text: xlsxText{R: []xlsxR{}}, @@ -328,8 +346,9 @@ func (f *File) addComment(commentsXML string, comment Comment) { } cmt.Text.R = append(cmt.Text.R, r) } - comments.CommentList.Comment = append(comments.CommentList.Comment, cmt) - f.Comments[commentsXML] = comments + cmts.CommentList.Comment = append(cmts.CommentList.Comment, cmt) + f.Comments[commentsXML] = cmts + return err } // countComments provides a function to get comments files count storage in @@ -355,20 +374,18 @@ func (f *File) countComments() int { // decodeVMLDrawingReader provides a function to get the pointer to the // structure after deserialization of xl/drawings/vmlDrawing%d.xml. -func (f *File) decodeVMLDrawingReader(path string) *decodeVmlDrawing { - var err error - +func (f *File) decodeVMLDrawingReader(path string) (*decodeVmlDrawing, error) { if f.DecodeVMLDrawing[path] == nil { c, ok := f.Pkg.Load(path) if ok && c != nil { f.DecodeVMLDrawing[path] = new(decodeVmlDrawing) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c.([]byte)))). + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c.([]byte)))). Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF { - log.Printf("xml decode error: %s", err) + return nil, err } } } - return f.DecodeVMLDrawing[path] + return f.DecodeVMLDrawing[path], nil } // vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml @@ -384,19 +401,18 @@ func (f *File) vmlDrawingWriter() { // commentsReader provides a function to get the pointer to the structure // after deserialization of xl/comments%d.xml. -func (f *File) commentsReader(path string) *xlsxComments { - var err error +func (f *File) commentsReader(path string) (*xlsxComments, error) { if f.Comments[path] == nil { content, ok := f.Pkg.Load(path) if ok && content != nil { f.Comments[path] = new(xlsxComments) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). Decode(f.Comments[path]); err != nil && err != io.EOF { - log.Printf("xml decode error: %s", err) + return nil, err } } } - return f.Comments[path] + return f.Comments[path], nil } // commentsWriter provides a function to save xl/comments%d.xml after diff --git a/comment_test.go b/comment_test.go index 019dc3b8ed..ed445084ae 100644 --- a/comment_test.go +++ b/comment_test.go @@ -34,16 +34,37 @@ func TestAddComments(t *testing.T) { assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist") // Test add comment on with illegal cell reference assert.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + comments, err := f.GetComments() + assert.NoError(t, err) if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { - assert.Len(t, f.GetComments(), 2) + assert.Len(t, comments, 2) } f.Comments["xl/comments2.xml"] = nil f.Pkg.Store("xl/comments2.xml", []byte(xml.Header+`Excelize: Excelize: `)) - comments := f.GetComments() + comments, err = f.GetComments() + assert.NoError(t, err) assert.EqualValues(t, 2, len(comments["Sheet1"])) assert.EqualValues(t, 1, len(comments["Sheet2"])) - assert.EqualValues(t, len(NewFile().GetComments()), 0) + comments, err = NewFile().GetComments() + assert.NoError(t, err) + assert.EqualValues(t, len(comments), 0) + + // Test add comments with unsupported charset. + f.Comments["xl/comments2.xml"] = nil + f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset) + _, err = f.GetComments() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + + // Test add comments with unsupported charset. + f.Comments["xl/comments2.xml"] = nil + f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8") + + // Test add comments with unsupported charset style sheet. + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8") } func TestDeleteComment(t *testing.T) { @@ -61,19 +82,30 @@ func TestDeleteComment(t *testing.T) { assert.NoError(t, f.DeleteComment("Sheet2", "A40")) - assert.EqualValues(t, 5, len(f.GetComments()["Sheet2"])) - assert.EqualValues(t, len(NewFile().GetComments()), 0) + comments, err := f.GetComments() + assert.NoError(t, err) + assert.EqualValues(t, 5, len(comments["Sheet2"])) + + comments, err = NewFile().GetComments() + assert.NoError(t, err) + assert.EqualValues(t, len(comments), 0) // Test delete all comments in a worksheet assert.NoError(t, f.DeleteComment("Sheet2", "A41")) assert.NoError(t, f.DeleteComment("Sheet2", "C41")) assert.NoError(t, f.DeleteComment("Sheet2", "C42")) - assert.EqualValues(t, 0, len(f.GetComments()["Sheet2"])) + comments, err = f.GetComments() + assert.NoError(t, err) + assert.EqualValues(t, 0, len(comments["Sheet2"])) // Test delete comment on not exists worksheet assert.EqualError(t, f.DeleteComment("SheetN", "A1"), "sheet SheetN does not exist") // Test delete comment with worksheet part f.Pkg.Delete("xl/worksheets/sheet1.xml") assert.NoError(t, f.DeleteComment("Sheet1", "A22")) + + f.Comments["xl/comments2.xml"] = nil + f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.DeleteComment("Sheet2", "A41"), "XML syntax error on line 1: invalid UTF-8") } func TestDecodeVMLDrawingReader(t *testing.T) { @@ -85,9 +117,11 @@ func TestDecodeVMLDrawingReader(t *testing.T) { func TestCommentsReader(t *testing.T) { f := NewFile() + // Test read comments with unsupported charset. path := "xl/comments1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) - f.commentsReader(path) + _, err := f.commentsReader(path) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestCountComments(t *testing.T) { diff --git a/docProps.go b/docProps.go index 4ee46ad1d0..ebe929b03d 100644 --- a/docProps.go +++ b/docProps.go @@ -76,7 +76,6 @@ func (f *File) SetAppProps(appProperties *AppProperties) error { app = new(xlsxProperties) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsApp)))). Decode(app); err != nil && err != io.EOF { - err = newDecodeXMLError(err) return err } fields = []string{"Application", "ScaleCrop", "DocSecurity", "Company", "LinksUpToDate", "HyperlinksChanged", "AppVersion"} @@ -103,7 +102,6 @@ func (f *File) GetAppProps() (ret *AppProperties, err error) { app := new(xlsxProperties) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsApp)))). Decode(app); err != nil && err != io.EOF { - err = newDecodeXMLError(err) return } ret, err = &AppProperties{ @@ -182,7 +180,6 @@ func (f *File) SetDocProps(docProperties *DocProperties) error { core = new(decodeCoreProperties) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))). Decode(core); err != nil && err != io.EOF { - err = newDecodeXMLError(err) return err } newProps = &xlsxCoreProperties{ @@ -237,7 +234,6 @@ func (f *File) GetDocProps() (ret *DocProperties, err error) { if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))). Decode(core); err != nil && err != io.EOF { - err = newDecodeXMLError(err) return } ret, err = &DocProperties{ diff --git a/docProps_test.go b/docProps_test.go index 545059d8df..64b690ce7c 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -42,7 +42,7 @@ func TestSetAppProps(t *testing.T) { // Test unsupported charset f = NewFile() f.Pkg.Store(defaultXMLPathDocPropsApp, MacintoshCyrillicCharset) - assert.EqualError(t, f.SetAppProps(&AppProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.SetAppProps(&AppProperties{}), "XML syntax error on line 1: invalid UTF-8") } func TestGetAppProps(t *testing.T) { @@ -58,11 +58,11 @@ func TestGetAppProps(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.Close()) - // Test unsupported charset + // Test get application properties with unsupported charset. f = NewFile() f.Pkg.Store(defaultXMLPathDocPropsApp, MacintoshCyrillicCharset) _, err = f.GetAppProps() - assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestSetDocProps(t *testing.T) { @@ -94,7 +94,7 @@ func TestSetDocProps(t *testing.T) { // Test unsupported charset f = NewFile() f.Pkg.Store(defaultXMLPathDocPropsCore, MacintoshCyrillicCharset) - assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.SetDocProps(&DocProperties{}), "XML syntax error on line 1: invalid UTF-8") } func TestGetDocProps(t *testing.T) { @@ -110,9 +110,9 @@ func TestGetDocProps(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.Close()) - // Test unsupported charset + // Test get workbook properties with unsupported charset. f = NewFile() f.Pkg.Store(defaultXMLPathDocPropsCore, MacintoshCyrillicCharset) _, err = f.GetDocProps() - assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } diff --git a/drawing.go b/drawing.go index 4fe575bafa..08b7aac767 100644 --- a/drawing.go +++ b/drawing.go @@ -15,7 +15,6 @@ import ( "bytes" "encoding/xml" "io" - "log" "reflect" "strconv" "strings" @@ -1194,7 +1193,7 @@ func (f *File) drawPlotAreaTxPr(opts *chartAxisOptions) *cTxPr { // the problem that the label structure is changed after serialization and // deserialization, two different structures: decodeWsDr and encodeWsDr are // defined. -func (f *File) drawingParser(path string) (*xlsxWsDr, int) { +func (f *File) drawingParser(path string) (*xlsxWsDr, int, error) { var ( err error ok bool @@ -1208,7 +1207,7 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { decodeWsDr := decodeWsDr{} if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). Decode(&decodeWsDr); err != nil && err != io.EOF { - log.Printf("xml decode error: %s", err) + return nil, 0, err } content.R = decodeWsDr.R for _, v := range decodeWsDr.AlternateContent { @@ -1238,7 +1237,7 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { } wsDr.Lock() defer wsDr.Unlock() - return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2 + return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2, nil } // addDrawingChart provides a function to add chart graphic frame by given @@ -1254,7 +1253,10 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI width = int(float64(width) * opts.XScale) height = int(float64(height) * opts.YScale) colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.OffsetX, opts.OffsetY, width, height) - content, cNvPrID := f.drawingParser(drawingXML) + content, cNvPrID, err := f.drawingParser(drawingXML) + if err != nil { + return err + } twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = opts.Positioning from := xlsxFrom{} @@ -1302,8 +1304,11 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI // addSheetDrawingChart provides a function to add chart graphic frame for // chartsheet by given sheet, drawingXML, width, height, relationship index // and format sets. -func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *pictureOptions) { - content, cNvPrID := f.drawingParser(drawingXML) +func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *pictureOptions) error { + content, cNvPrID, err := f.drawingParser(drawingXML) + if err != nil { + return err + } absoluteAnchor := xdrCellAnchor{ EditAs: opts.Positioning, Pos: &xlsxPoint2D{}, @@ -1336,6 +1341,7 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *pictureOpt } content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor) f.Drawings.Store(drawingXML, content) + return err } // deleteDrawing provides a function to delete chart graphic frame by given by @@ -1354,7 +1360,9 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) error "Chart": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic == nil }, "Pic": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic != nil }, } - wsDr, _ = f.drawingParser(drawingXML) + if wsDr, _, err = f.drawingParser(drawingXML); err != nil { + return err + } for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { if err = nil; wsDr.TwoCellAnchor[idx].From != nil && xdrCellAnchorFuncs[drawingType](wsDr.TwoCellAnchor[idx]) { if wsDr.TwoCellAnchor[idx].From.Col == col && wsDr.TwoCellAnchor[idx].From.Row == row { @@ -1367,7 +1375,6 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) error deTwoCellAnchor = new(decodeTwoCellAnchor) if err = f.xmlNewDecoder(strings.NewReader("" + wsDr.TwoCellAnchor[idx].GraphicFrame + "")). Decode(deTwoCellAnchor); err != nil && err != io.EOF { - err = newDecodeXMLError(err) return err } if err = nil; deTwoCellAnchor.From != nil && decodeTwoCellAnchorFuncs[drawingType](deTwoCellAnchor) { diff --git a/drawing_test.go b/drawing_test.go index e37b771fdd..5c090eb96d 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -15,6 +15,8 @@ import ( "encoding/xml" "sync" "testing" + + "github.com/stretchr/testify/assert" ) func TestDrawingParser(t *testing.T) { @@ -24,12 +26,15 @@ func TestDrawingParser(t *testing.T) { } f.Pkg.Store("charset", MacintoshCyrillicCharset) f.Pkg.Store("wsDr", []byte(xml.Header+``)) - // Test with one cell anchor - f.drawingParser("wsDr") - // Test with unsupported charset - f.drawingParser("charset") - // Test with alternate content + // Test with one cell anchor. + _, _, err := f.drawingParser("wsDr") + assert.NoError(t, err) + // Test with unsupported charset. + _, _, err = f.drawingParser("charset") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test with alternate content. f.Drawings = sync.Map{} f.Pkg.Store("wsDr", []byte(xml.Header+``)) - f.drawingParser("wsDr") + _, _, err = f.drawingParser("wsDr") + assert.NoError(t, err) } diff --git a/errors.go b/errors.go index f486ad4d15..1f7c6f8419 100644 --- a/errors.go +++ b/errors.go @@ -82,11 +82,6 @@ func newNotWorksheetError(name string) error { return fmt.Errorf("sheet %s is not a worksheet", name) } -// newDecodeXMLError defined the error message on decode XML error. -func newDecodeXMLError(err error) error { - return fmt.Errorf("xml decode error: %s", err) -} - // newStreamSetRowError defined the error message on the stream writer // receiving the non-ascending row number. func newStreamSetRowError(row int) error { diff --git a/excelize.go b/excelize.go index 987314bf81..256d42739b 100644 --- a/excelize.go +++ b/excelize.go @@ -177,11 +177,13 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) { for k, v := range file { f.Pkg.Store(k, v) } - f.CalcChain = f.calcChainReader() + if f.CalcChain, err = f.calcChainReader(); err != nil { + return f, err + } f.sheetMap = f.getSheetMap() - f.Styles = f.stylesReader() + f.Styles, err = f.stylesReader() f.Theme = f.themeReader() - return f, nil + return f, err } // parseOptions provides a function to parse the optional settings for open @@ -250,7 +252,6 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))). Decode(ws); err != nil && err != io.EOF { - err = newDecodeXMLError(err) return } err = nil diff --git a/excelize_test.go b/excelize_test.go index 74895f5369..cab994f817 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -10,6 +10,7 @@ import ( _ "image/gif" _ "image/jpeg" _ "image/png" + "io" "math" "os" "path/filepath" @@ -217,6 +218,28 @@ func TestOpenReader(t *testing.T) { _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password", UnzipXMLSizeLimit: UnzipSizeLimit + 1}) assert.EqualError(t, err, ErrWorkbookFileFormat.Error()) + // Test open workbook with unsupported charset internal calculation chain. + source, err := zip.OpenReader(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) + buf := new(bytes.Buffer) + zw := zip.NewWriter(buf) + for _, item := range source.File { + // The following statements can be simplified as zw.Copy(item) in go1.17 + writer, err := zw.Create(item.Name) + assert.NoError(t, err) + readerCloser, err := item.Open() + assert.NoError(t, err) + _, err = io.Copy(writer, readerCloser) + assert.NoError(t, err) + } + fi, err := zw.Create(defaultXMLPathCalcChain) + assert.NoError(t, err) + _, err = fi.Write(MacintoshCyrillicCharset) + assert.NoError(t, err) + assert.NoError(t, zw.Close()) + _, err = OpenReader(buf) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test open spreadsheet with unzip size limit. _, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipSizeLimit: 100}) assert.EqualError(t, err, newUnzipSizeLimitError(100).Error()) @@ -338,6 +361,9 @@ func TestAddDrawingVML(t *testing.T) { // Test addDrawingVML with illegal cell reference. f := NewFile() assert.EqualError(t, f.addDrawingVML(0, "", "*", 0, 0), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error()) + + f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) + assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", "A1", 0, 0), "XML syntax error on line 1: invalid UTF-8") } func TestSetCellHyperLink(t *testing.T) { @@ -1332,8 +1358,8 @@ func TestWorkSheetReader(t *testing.T) { f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) _, err := f.workSheetReader("Sheet1") - assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") - assert.EqualError(t, f.UpdateLinkedValue(), "xml decode error: XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.UpdateLinkedValue(), "XML syntax error on line 1: invalid UTF-8") // Test on no checked worksheet. f = NewFile() diff --git a/file.go b/file.go index fe5decaa0f..31eaa3d301 100644 --- a/file.go +++ b/file.go @@ -37,11 +37,11 @@ func NewFile() *File { f.Pkg.Store(defaultXMLPathWorkbook, []byte(xml.Header+templateWorkbook)) f.Pkg.Store(defaultXMLPathContentTypes, []byte(xml.Header+templateContentTypes)) f.SheetCount = 1 - f.CalcChain = f.calcChainReader() + f.CalcChain, _ = f.calcChainReader() f.Comments = make(map[string]*xlsxComments) f.ContentTypes = f.contentTypesReader() f.Drawings = sync.Map{} - f.Styles = f.stylesReader() + f.Styles, _ = f.stylesReader() f.DecodeVMLDrawing = make(map[string]*decodeVmlDrawing) f.VMLDrawing = make(map[string]*vmlDrawing) f.WorkBook = f.workbookReader() diff --git a/merge_test.go b/merge_test.go index e0b9210371..31f2cf4635 100644 --- a/merge_test.go +++ b/merge_test.go @@ -197,3 +197,9 @@ func TestFlatMergedCells(t *testing.T) { ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ""}}}} assert.EqualError(t, flatMergedCells(ws, [][]*xlsxMergeCell{}), "cannot convert cell \"\" to coordinates: invalid cell name \"\"") } + +func TestMergeCellsParser(t *testing.T) { + f := NewFile() + _, err := f.mergeCellsParser(&xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{nil}}}, "A1") + assert.NoError(t, err) +} diff --git a/picture.go b/picture.go index a7c1edb217..6cf1104c53 100644 --- a/picture.go +++ b/picture.go @@ -281,7 +281,10 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, col-- row-- colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height) - content, cNvPrID := f.drawingParser(drawingXML) + content, cNvPrID, err := f.drawingParser(drawingXML) + if err != nil { + return err + } twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = opts.Positioning from := xlsxFrom{} @@ -559,14 +562,15 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) deTwoCellAnchor *decodeTwoCellAnchor ) - wsDr, _ = f.drawingParser(drawingXML) + if wsDr, _, err = f.drawingParser(drawingXML); err != nil { + return + } if ret, buf = f.getPictureFromWsDr(row, col, drawingRelationships, wsDr); len(buf) > 0 { return } deWsDr = new(decodeWsDr) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). Decode(deWsDr); err != nil && err != io.EOF { - err = newDecodeXMLError(err) return } err = nil @@ -574,7 +578,6 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) deTwoCellAnchor = new(decodeTwoCellAnchor) if err = f.xmlNewDecoder(strings.NewReader("" + anchor.Content + "")). Decode(deTwoCellAnchor); err != nil && err != io.EOF { - err = newDecodeXMLError(err) return } if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic != nil { diff --git a/picture_test.go b/picture_test.go index c34780f719..65abf9ed8e 100644 --- a/picture_test.go +++ b/picture_test.go @@ -169,15 +169,25 @@ func TestGetPicture(t *testing.T) { assert.Empty(t, raw) f, err = prepareTestBook1() assert.NoError(t, err) - f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset) - _, _, err = f.getPicture(20, 5, "xl/drawings/drawing1.xml", "xl/drawings/_rels/drawing2.xml.rels") - assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") + + // Test get pictures with unsupported charset. + path := "xl/drawings/drawing1.xml" + f.Pkg.Store(path, MacintoshCyrillicCharset) + _, _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + f.Drawings.Delete(path) + _, _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestAddDrawingPicture(t *testing.T) { // Test addDrawingPicture with illegal cell reference. f := NewFile() assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", "", 0, 0, image.Config{}, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + + path := "xl/drawings/drawing1.xml" + f.Pkg.Store(path, MacintoshCyrillicCharset) + assert.EqualError(t, f.addDrawingPicture("sheet1", path, "A1", "", "", 0, 0, image.Config{}, &pictureOptions{}), "XML syntax error on line 1: invalid UTF-8") } func TestAddPictureFromBytes(t *testing.T) { diff --git a/rows.go b/rows.go index bfea398f97..34a227f8e9 100644 --- a/rows.go +++ b/rows.go @@ -16,7 +16,6 @@ import ( "encoding/xml" "fmt" "io" - "log" "math" "os" "strconv" @@ -139,7 +138,10 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) { } var rowIterator rowXMLIterator var token xml.Token - rows.rawCellValue, rows.sst = parseOptions(opts...).RawCellValue, rows.f.sharedStringsReader() + rows.rawCellValue = parseOptions(opts...).RawCellValue + if rows.sst, rowIterator.err = rows.f.sharedStringsReader(); rowIterator.err != nil { + return rowIterator.cells, rowIterator.err + } for { if rows.token != nil { token = rows.token @@ -160,21 +162,21 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) { rows.seekRowOpts = extractRowOpts(xmlElement.Attr) if rows.curRow > rows.seekRow { rows.token = nil - return rowIterator.columns, rowIterator.err + return rowIterator.cells, rowIterator.err } } if rows.rowXMLHandler(&rowIterator, &xmlElement, rows.rawCellValue); rowIterator.err != nil { rows.token = nil - return rowIterator.columns, rowIterator.err + return rowIterator.cells, rowIterator.err } rows.token = nil case xml.EndElement: if xmlElement.Name.Local == "sheetData" { - return rowIterator.columns, rowIterator.err + return rowIterator.cells, rowIterator.err } } } - return rowIterator.columns, rowIterator.err + return rowIterator.cells, rowIterator.err } // extractRowOpts extract row element attributes. @@ -211,10 +213,10 @@ func (err ErrSheetNotExist) Error() string { // rowXMLIterator defined runtime use field for the worksheet row SAX parser. type rowXMLIterator struct { - err error - inElement string - cellCol int - columns []string + err error + inElement string + cellCol, cellRow int + cells []string } // rowXMLHandler parse the row XML element of the worksheet. @@ -228,9 +230,9 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta return } } - blank := rowIterator.cellCol - len(rowIterator.columns) + blank := rowIterator.cellCol - len(rowIterator.cells) if val, _ := colCell.getValueFrom(rows.f, rows.sst, raw); val != "" || colCell.F != nil { - rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val) + rowIterator.cells = append(appendSpace(blank, rowIterator.cells), val) } } } @@ -409,7 +411,7 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { // sharedStringsReader provides a function to get the pointer to the structure // after deserialization of xl/sharedStrings.xml. -func (f *File) sharedStringsReader() *xlsxSST { +func (f *File) sharedStringsReader() (*xlsxSST, error) { var err error f.Lock() defer f.Unlock() @@ -419,7 +421,7 @@ func (f *File) sharedStringsReader() *xlsxSST { ss := f.readXML(defaultXMLPathSharedStrings) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(ss))). Decode(&sharedStrings); err != nil && err != io.EOF { - log.Printf("xml decode error: %s", err) + return f.SharedStrings, err } if sharedStrings.Count == 0 { sharedStrings.Count = len(sharedStrings.SI) @@ -437,14 +439,14 @@ func (f *File) sharedStringsReader() *xlsxSST { rels := f.relsReader(relPath) for _, rel := range rels.Relationships { if rel.Target == "/xl/sharedStrings.xml" { - return f.SharedStrings + return f.SharedStrings, nil } } // Update workbook.xml.rels f.addRels(relPath, SourceRelationshipSharedStrings, "/xl/sharedStrings.xml", "") } - return f.SharedStrings + return f.SharedStrings, nil } // SetRowVisible provides a function to set visible of a single row by given @@ -800,7 +802,10 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error { if end > TotalRows { return ErrMaxRows } - s := f.stylesReader() + s, err := f.stylesReader() + if err != nil { + return err + } s.Lock() defer s.Unlock() if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { diff --git a/rows_test.go b/rows_test.go index 81572e1852..5317c222de 100644 --- a/rows_test.go +++ b/rows_test.go @@ -55,7 +55,7 @@ func TestRows(t *testing.T) { value, err := f.GetCellValue("Sheet1", "A19") assert.NoError(t, err) assert.Equal(t, "Total:", value) - // Test load shared string table to memory + // Test load shared string table to memory. err = f.SetCellValue("Sheet1", "A19", "A19") assert.NoError(t, err) value, err = f.GetCellValue("Sheet1", "A19") @@ -63,6 +63,14 @@ func TestRows(t *testing.T) { assert.Equal(t, "A19", value) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRow.xlsx"))) assert.NoError(t, f.Close()) + + // Test rows iterator with unsupported charset shared strings table. + f.SharedStrings = nil + f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) + rows, err = f.Rows(sheet2) + assert.NoError(t, err) + _, err = rows.Columns() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestRowsIterator(t *testing.T) { @@ -225,6 +233,7 @@ func TestColumns(t *testing.T) { func TestSharedStringsReader(t *testing.T) { f := NewFile() + // Test read shared string with unsupported charset. f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) f.sharedStringsReader() si := xlsxSI{} @@ -965,12 +974,16 @@ func TestSetRowStyle(t *testing.T) { cellStyleID, err := f.GetCellStyle("Sheet1", "B2") assert.NoError(t, err) assert.Equal(t, style2, cellStyleID) - // Test cell inheritance rows style + // Test cell inheritance rows style. assert.NoError(t, f.SetCellValue("Sheet1", "C1", nil)) cellStyleID, err = f.GetCellStyle("Sheet1", "C1") assert.NoError(t, err) assert.Equal(t, style2, cellStyleID) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRowStyle.xlsx"))) + // Test set row style with unsupported charset style sheet. + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, cellStyleID), "XML syntax error on line 1: invalid UTF-8") } func TestNumberFormats(t *testing.T) { diff --git a/shape.go b/shape.go index 9f250d79eb..6f7c8fd4d5 100644 --- a/shape.go +++ b/shape.go @@ -305,8 +305,7 @@ func (f *File) AddShape(sheet, cell, opts string) error { f.addSheetDrawing(sheet, rID) f.addSheetNameSpace(sheet, SourceRelationship) } - err = f.addDrawingShape(sheet, drawingXML, cell, options) - if err != nil { + if err = f.addDrawingShape(sheet, drawingXML, cell, options); err != nil { return err } f.addContentTypePart(drawingID, "drawings") @@ -328,7 +327,10 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOption colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.Format.OffsetX, opts.Format.OffsetY, width, height) - content, cNvPrID := f.drawingParser(drawingXML) + content, cNvPrID, err := f.drawingParser(drawingXML) + if err != nil { + return err + } twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = opts.Format.Positioning from := xlsxFrom{} @@ -385,6 +387,10 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOption W: f.ptToEMUs(opts.Line.Width), } } + defaultFont, err := f.GetDefaultFont() + if err != nil { + return err + } if len(opts.Paragraph) < 1 { opts.Paragraph = []shapeParagraphOptions{ { @@ -392,7 +398,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOption Bold: false, Italic: false, Underline: "none", - Family: f.GetDefaultFont(), + Family: defaultFont, Size: 11, Color: "#000000", }, diff --git a/shape_test.go b/shape_test.go index 829a9e5e46..9d1da8a07f 100644 --- a/shape_test.go +++ b/shape_test.go @@ -87,4 +87,15 @@ func TestAddShape(t *testing.T) { } }`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) + // Test set row style with unsupported charset style sheet. + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + assert.EqualError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`), "XML syntax error on line 1: invalid UTF-8") +} + +func TestAddDrawingShape(t *testing.T) { + f := NewFile() + path := "xl/drawings/drawing1.xml" + f.Pkg.Store(path, MacintoshCyrillicCharset) + assert.EqualError(t, f.addDrawingShape("sheet1", path, "A1", &shapeOptions{}), "XML syntax error on line 1: invalid UTF-8") } diff --git a/sheet.go b/sheet.go index 1346801f77..0616d95b3c 100644 --- a/sheet.go +++ b/sheet.go @@ -881,10 +881,12 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, var ( cellName, inElement string cellCol, row int - d *xlsxSST + sst *xlsxSST ) - d = f.sharedStringsReader() + if sst, err = f.sharedStringsReader(); err != nil { + return + } decoder := f.xmlNewDecoder(bytes.NewReader(f.readBytes(name))) for { var token xml.Token @@ -907,7 +909,7 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, if inElement == "c" { colCell := xlsxC{} _ = decoder.DecodeElement(&colCell, &xmlElement) - val, _ := colCell.getValueFrom(f, d, false) + val, _ := colCell.getValueFrom(f, sst, false) if regSearch { regex := regexp.MustCompile(value) if !regex.MatchString(val) { diff --git a/sheet_test.go b/sheet_test.go index 4e1e44818b..4b9d31eccc 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -91,6 +91,12 @@ func TestSearchSheet(t *testing.T) { result, err = f.SearchSheet("Sheet1", "A") assert.EqualError(t, err, "invalid cell reference [1, 0]") assert.Equal(t, []string(nil), result) + + // Test search sheet with unsupported charset shared strings table. + f.SharedStrings = nil + f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) + _, err = f.SearchSheet("Sheet1", "A") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestSetPageLayout(t *testing.T) { diff --git a/stream_test.go b/stream_test.go index 040eee0783..65af283eab 100644 --- a/stream_test.go +++ b/stream_test.go @@ -106,12 +106,12 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, streamWriter.rawData.tmp.Close()) assert.NoError(t, os.Remove(streamWriter.rawData.tmp.Name())) - // Test unsupported charset + // Test create stream writer with unsupported charset. file = NewFile() file.Sheet.Delete("xl/worksheets/sheet1.xml") file.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) _, err = file.NewStreamWriter("Sheet1") - assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, file.Close()) // Test read cell. diff --git a/styles.go b/styles.go index f7d00e19a6..08d6b0c947 100644 --- a/styles.go +++ b/styles.go @@ -1037,15 +1037,15 @@ func formatToE(v, format string, date1904 bool) string { // stylesReader provides a function to get the pointer to the structure after // deserialization of xl/styles.xml. -func (f *File) stylesReader() *xlsxStyleSheet { +func (f *File) stylesReader() (*xlsxStyleSheet, error) { if f.Styles == nil { f.Styles = new(xlsxStyleSheet) if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathStyles)))). Decode(f.Styles); err != nil && err != io.EOF { - log.Printf("xml decode error: %s", err) + return f.Styles, err } } - return f.Styles + return f.Styles, nil } // styleSheetWriter provides a function to save xl/styles.xml after serialize @@ -1965,9 +1965,12 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { // // Cell Sheet1!A6 in the Excel Application: martes, 04 de Julio de 2017 func (f *File) NewStyle(style interface{}) (int, error) { - var fs *Style - var err error - var cellXfsID, fontID, borderID, fillID int + var ( + fs *Style + font *xlsxFont + err error + cellXfsID, fontID, borderID, fillID int + ) fs, err = parseFormatStyleSet(style) if err != nil { return cellXfsID, err @@ -1975,21 +1978,25 @@ func (f *File) NewStyle(style interface{}) (int, error) { if fs.DecimalPlaces == 0 { fs.DecimalPlaces = 2 } - s := f.stylesReader() + s, err := f.stylesReader() + if err != nil { + return cellXfsID, err + } s.Lock() defer s.Unlock() // check given style already exist. - if cellXfsID = f.getStyleID(s, fs); cellXfsID != -1 { + if cellXfsID, err = f.getStyleID(s, fs); err != nil || cellXfsID != -1 { return cellXfsID, err } numFmtID := newNumFmt(s, fs) if fs.Font != nil { - fontID = f.getFontID(s, fs) + fontID, _ = f.getFontID(s, fs) if fontID == -1 { s.Fonts.Count++ - s.Fonts.Font = append(s.Fonts.Font, f.newFont(fs)) + font, _ = f.newFont(fs) + s.Fonts.Font = append(s.Fonts.Font, font) fontID = s.Fonts.Count - 1 } } @@ -2065,12 +2072,19 @@ var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ // getStyleID provides a function to get styleID by given style. If given // style does not exist, will return -1. -func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (styleID int) { - styleID = -1 +func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (int, error) { + var ( + err error + fontID int + styleID = -1 + ) if ss.CellXfs == nil { - return + return styleID, err + } + numFmtID, borderID, fillID := getNumFmtID(ss, style), getBorderID(ss, style), getFillID(ss, style) + if fontID, err = f.getFontID(ss, style); err != nil { + return styleID, err } - numFmtID, borderID, fillID, fontID := getNumFmtID(ss, style), getBorderID(ss, style), getFillID(ss, style), f.getFontID(ss, style) if style.CustomNumFmt != nil { numFmtID = getCustomNumFmtID(ss, style) } @@ -2082,10 +2096,10 @@ func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (styleID int) { getXfIDFuncs["alignment"](0, xf, style) && getXfIDFuncs["protection"](0, xf, style) { styleID = xfID - return + return styleID, err } } - return + return styleID, err } // NewConditionalStyle provides a function to create style for conditional @@ -2093,7 +2107,10 @@ func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (styleID int) { // function. Note that the color field uses RGB color code and only support to // set font, fills, alignment and borders currently. func (f *File) NewConditionalStyle(style string) (int, error) { - s := f.stylesReader() + s, err := f.stylesReader() + if err != nil { + return 0, err + } fs, err := parseFormatStyleSet(style) if err != nil { return 0, err @@ -2108,7 +2125,7 @@ func (f *File) NewConditionalStyle(style string) (int, error) { dxf.Border = newBorders(fs) } if fs.Font != nil { - dxf.Font = f.newFont(fs) + dxf.Font, _ = f.newFont(fs) } dxfStr, _ := xml.Marshal(dxf) if s.Dxfs == nil { @@ -2123,41 +2140,56 @@ func (f *File) NewConditionalStyle(style string) (int, error) { // GetDefaultFont provides the default font name currently set in the // workbook. The spreadsheet generated by excelize default font is Calibri. -func (f *File) GetDefaultFont() string { - font := f.readDefaultFont() - return *font.Name.Val +func (f *File) GetDefaultFont() (string, error) { + font, err := f.readDefaultFont() + if err != nil { + return "", err + } + return *font.Name.Val, err } // SetDefaultFont changes the default font in the workbook. -func (f *File) SetDefaultFont(fontName string) { - font := f.readDefaultFont() +func (f *File) SetDefaultFont(fontName string) error { + font, err := f.readDefaultFont() + if err != nil { + return err + } font.Name.Val = stringPtr(fontName) - s := f.stylesReader() + s, _ := f.stylesReader() s.Fonts.Font[0] = font custom := true s.CellStyles.CellStyle[0].CustomBuiltIn = &custom + return err } // readDefaultFont provides an un-marshalled font value. -func (f *File) readDefaultFont() *xlsxFont { - s := f.stylesReader() - return s.Fonts.Font[0] +func (f *File) readDefaultFont() (*xlsxFont, error) { + s, err := f.stylesReader() + if err != nil { + return nil, err + } + return s.Fonts.Font[0], err } // getFontID provides a function to get font ID. // If given font does not exist, will return -1. -func (f *File) getFontID(styleSheet *xlsxStyleSheet, style *Style) (fontID int) { - fontID = -1 +func (f *File) getFontID(styleSheet *xlsxStyleSheet, style *Style) (int, error) { + var err error + fontID := -1 if styleSheet.Fonts == nil || style.Font == nil { - return + return fontID, err } for idx, fnt := range styleSheet.Fonts.Font { - if reflect.DeepEqual(*fnt, *f.newFont(style)) { + font, err := f.newFont(style) + if err != nil { + return fontID, err + } + if reflect.DeepEqual(*fnt, *font) { fontID = idx - return + return fontID, err } } - return + return fontID, err } // newFontColor set font color by given styles. @@ -2190,7 +2222,8 @@ func newFontColor(font *Font) *xlsxColor { // newFont provides a function to add font style by given cell format // settings. -func (f *File) newFont(style *Style) *xlsxFont { +func (f *File) newFont(style *Style) (*xlsxFont, error) { + var err error if style.Font.Size < MinFontSize { style.Font.Size = 11 } @@ -2207,7 +2240,9 @@ func (f *File) newFont(style *Style) *xlsxFont { fnt.I = &attrValBool{Val: &style.Font.Italic} } if *fnt.Name.Val == "" { - *fnt.Name.Val = f.GetDefaultFont() + if *fnt.Name.Val, err = f.GetDefaultFont(); err != nil { + return &fnt, err + } } if style.Font.Strike { fnt.Strike = &attrValBool{Val: &style.Font.Strike} @@ -2215,7 +2250,7 @@ func (f *File) newFont(style *Style) *xlsxFont { if idx := inStrSlice(supportedUnderlineTypes, style.Font.Underline, true); idx != -1 { fnt.U = &attrValString{Val: stringPtr(supportedUnderlineTypes[idx])} } - return &fnt + return &fnt, err } // getNumFmtID provides a function to get number format code ID. @@ -2754,7 +2789,10 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { ws.Lock() defer ws.Unlock() - s := f.stylesReader() + s, err := f.stylesReader() + if err != nil { + return err + } s.Lock() defer s.Unlock() if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { diff --git a/styles_test.go b/styles_test.go index 487a6df63e..605ad07e4d 100644 --- a/styles_test.go +++ b/styles_test.go @@ -30,7 +30,8 @@ func TestStyleFill(t *testing.T) { styleID, err := xl.NewStyle(testCase.format) assert.NoError(t, err) - styles := xl.stylesReader() + styles, err := xl.stylesReader() + assert.NoError(t, err) style := styles.CellXfs.Xf[styleID] if testCase.expectFill { assert.NotEqual(t, *style.FillID, 0, testCase.label) @@ -220,7 +221,8 @@ func TestNewStyle(t *testing.T) { f := NewFile() styleID, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) assert.NoError(t, err) - styles := f.stylesReader() + styles, err := f.stylesReader() + assert.NoError(t, err) fontID := styles.CellXfs.Xf[styleID].FontID font := styles.Fonts.Font[*fontID] assert.Contains(t, *font.Name.Val, "Times New Roman", "Stored font should contain font name") @@ -238,7 +240,7 @@ func TestNewStyle(t *testing.T) { _, err = f.NewStyle(&Style{Font: &Font{Size: MaxFontSize + 1}}) assert.EqualError(t, err, ErrFontSize.Error()) - // new numeric custom style + // Test create numeric custom style. numFmt := "####;####" f.Styles.NumFmts = nil styleID, err = f.NewStyle(&Style{ @@ -254,7 +256,7 @@ func TestNewStyle(t *testing.T) { nf := f.Styles.CellXfs.Xf[styleID] assert.Equal(t, 164, *nf.NumFmtID) - // new currency custom style + // Test create currency custom style. f.Styles.NumFmts = nil styleID, err = f.NewStyle(&Style{ Lang: "ko-kr", @@ -271,7 +273,7 @@ func TestNewStyle(t *testing.T) { nf = f.Styles.CellXfs.Xf[styleID] assert.Equal(t, 32, *nf.NumFmtID) - // Test set build-in scientific number format + // Test set build-in scientific number format. styleID, err = f.NewStyle(&Style{NumFmt: 11}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "B1", styleID)) @@ -281,7 +283,7 @@ func TestNewStyle(t *testing.T) { assert.Equal(t, [][]string{{"1.23E+00", "1.23E+00"}}, rows) f = NewFile() - // Test currency number format + // Test currency number format. customNumFmt := "[$$-409]#,##0.00" style1, err := f.NewStyle(&Style{CustomNumFmt: &customNumFmt}) assert.NoError(t, err) @@ -306,21 +308,48 @@ func TestNewStyle(t *testing.T) { style5, err := f.NewStyle(&Style{NumFmt: 160, Lang: "zh-cn"}) assert.NoError(t, err) assert.Equal(t, 0, style5) + + // Test create style with unsupported charset style sheet. + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + _, err = f.NewStyle(&Style{NumFmt: 165}) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") +} + +func TestNewConditionalStyle(t *testing.T) { + f := NewFile() + // Test create conditional style with unsupported charset style sheet. + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + _, err := f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestGetDefaultFont(t *testing.T) { f := NewFile() - s := f.GetDefaultFont() + s, err := f.GetDefaultFont() + assert.NoError(t, err) assert.Equal(t, s, "Calibri", "Default font should be Calibri") + // Test get default font with unsupported charset style sheet. + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + _, err = f.GetDefaultFont() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestSetDefaultFont(t *testing.T) { f := NewFile() - f.SetDefaultFont("Arial") - styles := f.stylesReader() - s := f.GetDefaultFont() + assert.NoError(t, f.SetDefaultFont("Arial")) + styles, err := f.stylesReader() + assert.NoError(t, err) + s, err := f.GetDefaultFont() + assert.NoError(t, err) assert.Equal(t, s, "Arial", "Default font should change to Arial") assert.Equal(t, *styles.CellStyles.CellStyle[0].CustomBuiltIn, true) + // Test set default font with unsupported charset style sheet. + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetDefaultFont("Arial"), "XML syntax error on line 1: invalid UTF-8") } func TestStylesReader(t *testing.T) { @@ -328,7 +357,9 @@ func TestStylesReader(t *testing.T) { // Test read styles with unsupported charset. f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) - assert.EqualValues(t, new(xlsxStyleSheet), f.stylesReader()) + styles, err := f.stylesReader() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.EqualValues(t, new(xlsxStyleSheet), styles) } func TestThemeReader(t *testing.T) { @@ -346,14 +377,33 @@ func TestSetCellStyle(t *testing.T) { assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", -1), newInvalidStyleID(-1).Error()) // Test set cell style with not exists style ID. assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", 10), newInvalidStyleID(10).Error()) + // Test set cell style with unsupported charset style sheet. + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", 1), "XML syntax error on line 1: invalid UTF-8") } func TestGetStyleID(t *testing.T) { - assert.Equal(t, -1, NewFile().getStyleID(&xlsxStyleSheet{}, nil)) + f := NewFile() + styleID, err := f.getStyleID(&xlsxStyleSheet{}, nil) + assert.NoError(t, err) + assert.Equal(t, -1, styleID) + // Test get style ID with unsupported charset style sheet. + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + _, err = f.getStyleID(&xlsxStyleSheet{ + CellXfs: &xlsxCellXfs{}, + Fonts: &xlsxFonts{ + Font: []*xlsxFont{{}}, + }, + }, &Style{NumFmt: 0, Font: &Font{}}) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestGetFillID(t *testing.T) { - assert.Equal(t, -1, getFillID(NewFile().stylesReader(), &Style{Fill: Fill{Type: "unknown"}})) + styles, err := NewFile().stylesReader() + assert.NoError(t, err) + assert.Equal(t, -1, getFillID(styles, &Style{Fill: Fill{Type: "unknown"}})) } func TestThemeColor(t *testing.T) { From ac564afa56a691e378ab9bb04cb14bb283886a16 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 13 Nov 2022 00:40:04 +0800 Subject: [PATCH 130/213] Remove internal error log print, throw XML deserialize error --- calcchain.go | 5 +- calcchain_test.go | 7 +++ cell.go | 13 +++-- cell_test.go | 16 ++++-- chart.go | 20 ++++--- chart_test.go | 13 +++++ comment.go | 7 ++- comment_test.go | 3 +- excelize.go | 36 ++++++++----- excelize_test.go | 98 ++++++++++++++++++---------------- file.go | 15 +++--- file_test.go | 9 ++++ picture.go | 52 ++++++++++++------ picture_test.go | 30 +++++++++++ pivotTable.go | 10 ++-- pivotTable_test.go | 9 ++++ rows.go | 9 +++- rows_test.go | 17 ++++-- shape.go | 3 +- shape_test.go | 7 ++- sheet.go | 129 +++++++++++++++++++++++++-------------------- sheet_test.go | 37 +++++++++++++ stream.go | 34 +++++++----- stream_test.go | 16 ++++-- styles.go | 9 ++-- styles_test.go | 4 +- table.go | 8 +-- table_test.go | 6 ++- templates.go | 1 + workbook.go | 26 +++++---- workbook_test.go | 12 ++++- 31 files changed, 458 insertions(+), 203 deletions(-) diff --git a/calcchain.go b/calcchain.go index 3aa5d812f3..5e511dc2eb 100644 --- a/calcchain.go +++ b/calcchain.go @@ -54,7 +54,10 @@ func (f *File) deleteCalcChain(index int, cell string) error { if len(calc.C) == 0 { f.CalcChain = nil f.Pkg.Delete(defaultXMLPathCalcChain) - content := f.contentTypesReader() + content, err := f.contentTypesReader() + if err != nil { + return err + } content.Lock() defer content.Unlock() for k, v := range content.Overrides { diff --git a/calcchain_test.go b/calcchain_test.go index fae3a5130b..9eec804ca9 100644 --- a/calcchain_test.go +++ b/calcchain_test.go @@ -33,7 +33,14 @@ func TestDeleteCalcChain(t *testing.T) { formulaType, ref := STCellFormulaTypeShared, "C1:C5" assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType})) + + // Test delete calculation chain with unsupported charset calculation chain. f.CalcChain = nil f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset) assert.EqualError(t, f.SetCellValue("Sheet1", "C1", true), "XML syntax error on line 1: invalid UTF-8") + + // Test delete calculation chain with unsupported charset content types. + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteCalcChain(1, "A1"), "XML syntax error on line 1: invalid UTF-8") } diff --git a/cell.go b/cell.go index cbb7932a93..a0a281845d 100644 --- a/cell.go +++ b/cell.go @@ -241,11 +241,14 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error { ws.Lock() c.S = f.prepareCellStyle(ws, col, row, c.S) ws.Unlock() - date1904, wb := false, f.workbookReader() + var date1904, isNum bool + wb, err := f.workbookReader() + if err != nil { + return err + } if wb != nil && wb.WorkbookPr != nil { date1904 = wb.WorkbookPr.Date1904 } - var isNum bool if isNum, err = c.setCellTime(value, date1904); err != nil { return err } @@ -1320,7 +1323,11 @@ func (f *File) formattedValue(s int, v string, raw bool) (string, error) { if styleSheet.CellXfs.Xf[s].NumFmtID != nil { numFmtID = *styleSheet.CellXfs.Xf[s].NumFmtID } - date1904, wb := false, f.workbookReader() + date1904 := false + wb, err := f.workbookReader() + if err != nil { + return v, err + } if wb != nil && wb.WorkbookPr != nil { date1904 = wb.WorkbookPr.Date1904 } diff --git a/cell_test.go b/cell_test.go index 18bc10113c..40bab9b6d1 100644 --- a/cell_test.go +++ b/cell_test.go @@ -173,7 +173,7 @@ func TestSetCellValue(t *testing.T) { f := NewFile() assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Duration(1e13)), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - // Test set cell value with column and row style inherit + // Test set cell value with column and row style inherit. style1, err := f.NewStyle(&Style{NumFmt: 2}) assert.NoError(t, err) style2, err := f.NewStyle(&Style{NumFmt: 9}) @@ -189,10 +189,14 @@ func TestSetCellValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "0.50", B2) - // Test set cell value with unsupported charset shared strings table + // Test set cell value with unsupported charset shared strings table. f.SharedStrings = nil f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) assert.EqualError(t, f.SetCellValue("Sheet1", "A1", "A1"), "XML syntax error on line 1: invalid UTF-8") + // Test set cell value with unsupported charset workbook. + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetCellValue("Sheet1", "A1", time.Now().UTC()), "XML syntax error on line 1: invalid UTF-8") } func TestSetCellValues(t *testing.T) { @@ -204,7 +208,7 @@ func TestSetCellValues(t *testing.T) { assert.NoError(t, err) assert.Equal(t, v, "12/31/10 00:00") - // Test date value lower than min date supported by Excel + // Test date value lower than min date supported by Excel. err = f.SetCellValue("Sheet1", "A1", time.Date(1600, time.December, 31, 0, 0, 0, 0, time.UTC)) assert.NoError(t, err) @@ -782,6 +786,12 @@ func TestFormattedValue(t *testing.T) { assert.Equal(t, "0_0", fn("0_0", "", false)) } + // Test format value with unsupported charset workbook. + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + _, err = f.formattedValue(1, "43528", false) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test format value with unsupported charset style sheet. f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) diff --git a/chart.go b/chart.go index be6ddd8f9f..ec5e468eea 100644 --- a/chart.go +++ b/chart.go @@ -927,8 +927,10 @@ func (f *File) AddChart(sheet, cell, opts string, combo ...string) error { return err } f.addChart(options, comboCharts) - f.addContentTypePart(chartID, "chart") - f.addContentTypePart(drawingID, "drawings") + if err = f.addContentTypePart(chartID, "chart"); err != nil { + return err + } + _ = f.addContentTypePart(drawingID, "drawings") f.addSheetNameSpace(sheet, SourceRelationship) return err } @@ -952,7 +954,7 @@ func (f *File) AddChartSheet(sheet, opts string, combo ...string) error { }, } f.SheetCount++ - wb := f.workbookReader() + wb, _ := f.workbookReader() sheetID := 0 for _, v := range wb.Sheets.Sheet { if v.SheetID > sheetID { @@ -969,11 +971,15 @@ func (f *File) AddChartSheet(sheet, opts string, combo ...string) error { f.prepareChartSheetDrawing(&cs, drawingID, sheet) drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") - f.addSheetDrawingChart(drawingXML, drawingRID, &options.Format) + if err = f.addSheetDrawingChart(drawingXML, drawingRID, &options.Format); err != nil { + return err + } f.addChart(options, comboCharts) - f.addContentTypePart(chartID, "chart") - f.addContentTypePart(sheetID, "chartsheet") - f.addContentTypePart(drawingID, "drawings") + if err = f.addContentTypePart(chartID, "chart"); err != nil { + return err + } + _ = f.addContentTypePart(sheetID, "chartsheet") + _ = f.addContentTypePart(drawingID, "drawings") // Update workbook.xml.rels rID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipChartsheet, fmt.Sprintf("/xl/chartsheets/sheet%d.xml", sheetID), "") // Update workbook.xml diff --git a/chart_test.go b/chart_test.go index dac724a3d2..efce55dcd0 100644 --- a/chart_test.go +++ b/chart_test.go @@ -226,6 +226,11 @@ func TestAddChart(t *testing.T) { // Test add combo chart with unsupported chart type assert.EqualError(t, f.AddChart("Sheet2", "BD64", `{"type":"barOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bar of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`, `{"type":"unknown","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bar of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`), "unsupported chart type unknown") assert.NoError(t, f.Close()) + + // Test add chart with unsupported charset content types. + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"title":{"name":"2D Column Chart"}}`), "XML syntax error on line 1: invalid UTF-8") } func TestAddChartSheet(t *testing.T) { @@ -259,6 +264,14 @@ func TestAddChartSheet(t *testing.T) { assert.NoError(t, f.UpdateLinkedValue()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChartSheet.xlsx"))) + // Test add chart sheet with unsupported charset drawing XML. + f.Pkg.Store("xl/drawings/drawing4.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.AddChartSheet("Chart3", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"title":{"name":"2D Column Chart"}}`), "XML syntax error on line 1: invalid UTF-8") + // Test add chart sheet with unsupported charset content types. + f = NewFile() + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.AddChartSheet("Chart4", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"title":{"name":"2D Column Chart"}}`), "XML syntax error on line 1: invalid UTF-8") } func TestDeleteChart(t *testing.T) { diff --git a/comment.go b/comment.go index eec5fa63da..ae62c37858 100644 --- a/comment.go +++ b/comment.go @@ -69,8 +69,8 @@ func (f *File) GetComments() (map[string][]Comment, error) { // getSheetComments provides the method to get the target comment reference by // given worksheet file path. func (f *File) getSheetComments(sheetFile string) string { - rels := "xl/worksheets/_rels/" + sheetFile + ".rels" - if sheetRels := f.relsReader(rels); sheetRels != nil { + rels, _ := f.relsReader("xl/worksheets/_rels/" + sheetFile + ".rels") + if sheetRels := rels; sheetRels != nil { sheetRels.Lock() defer sheetRels.Unlock() for _, v := range sheetRels.Relationships { @@ -135,8 +135,7 @@ func (f *File) AddComment(sheet string, comment Comment) error { if err = f.addComment(commentsXML, comment); err != nil { return err } - f.addContentTypePart(commentID, "comments") - return err + return f.addContentTypePart(commentID, "comments") } // DeleteComment provides the method to delete comment in a sheet by given diff --git a/comment_test.go b/comment_test.go index ed445084ae..ead393945c 100644 --- a/comment_test.go +++ b/comment_test.go @@ -112,7 +112,8 @@ func TestDecodeVMLDrawingReader(t *testing.T) { f := NewFile() path := "xl/drawings/vmlDrawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) - f.decodeVMLDrawingReader(path) + _, err := f.decodeVMLDrawingReader(path) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestCommentsReader(t *testing.T) { diff --git a/excelize.go b/excelize.go index 256d42739b..f4c7a255a8 100644 --- a/excelize.go +++ b/excelize.go @@ -181,8 +181,10 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) { return f, err } f.sheetMap = f.getSheetMap() - f.Styles, err = f.stylesReader() - f.Theme = f.themeReader() + if f.Styles, err = f.stylesReader(); err != nil { + return f, err + } + f.Theme, err = f.themeReader() return f, err } @@ -335,7 +337,7 @@ func checkSheetR0(ws *xlsxWorksheet, sheetData *xlsxSheetData, r0 *xlsxRow) { // setRels provides a function to set relationships by given relationship ID, // XML path, relationship type, target and target mode. func (f *File) setRels(rID, relPath, relType, target, targetMode string) int { - rels := f.relsReader(relPath) + rels, _ := f.relsReader(relPath) if rels == nil || rID == "" { return f.addRels(relPath, relType, target, targetMode) } @@ -360,7 +362,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { uniqPart := map[string]string{ SourceRelationshipSharedStrings: "/xl/sharedStrings.xml", } - rels := f.relsReader(relPath) + rels, _ := f.relsReader(relPath) if rels == nil { rels = &xlsxRelationships{} } @@ -418,7 +420,10 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { // // func (f *File) UpdateLinkedValue() error { - wb := f.workbookReader() + wb, err := f.workbookReader() + if err != nil { + return err + } // recalculate formulas wb.CalcPr = nil for _, name := range f.GetSheetList() { @@ -465,12 +470,15 @@ func (f *File) AddVBAProject(bin string) error { if path.Ext(bin) != ".bin" { return ErrAddVBAProject } - wb := f.relsReader(f.getWorkbookRelsPath()) - wb.Lock() - defer wb.Unlock() + rels, err := f.relsReader(f.getWorkbookRelsPath()) + if err != nil { + return err + } + rels.Lock() + defer rels.Unlock() var rID int var ok bool - for _, rel := range wb.Relationships { + for _, rel := range rels.Relationships { if rel.Target == "vbaProject.bin" && rel.Type == SourceRelationshipVBAProject { ok = true continue @@ -482,7 +490,7 @@ func (f *File) AddVBAProject(bin string) error { } rID++ if !ok { - wb.Relationships = append(wb.Relationships, xlsxRelationship{ + rels.Relationships = append(rels.Relationships, xlsxRelationship{ ID: "rId" + strconv.Itoa(rID), Target: "vbaProject.bin", Type: SourceRelationshipVBAProject, @@ -495,9 +503,12 @@ func (f *File) AddVBAProject(bin string) error { // setContentTypePartProjectExtensions provides a function to set the content // type for relationship parts and the main document part. -func (f *File) setContentTypePartProjectExtensions(contentType string) { +func (f *File) setContentTypePartProjectExtensions(contentType string) error { var ok bool - content := f.contentTypesReader() + content, err := f.contentTypesReader() + if err != nil { + return err + } content.Lock() defer content.Unlock() for _, v := range content.Defaults { @@ -516,4 +527,5 @@ func (f *File) setContentTypePartProjectExtensions(contentType string) { ContentType: ContentTypeVBA, }) } + return err } diff --git a/excelize_test.go b/excelize_test.go index cab994f817..ece74b2f4b 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -219,26 +219,31 @@ func TestOpenReader(t *testing.T) { assert.EqualError(t, err, ErrWorkbookFileFormat.Error()) // Test open workbook with unsupported charset internal calculation chain. - source, err := zip.OpenReader(filepath.Join("test", "Book1.xlsx")) - assert.NoError(t, err) - buf := new(bytes.Buffer) - zw := zip.NewWriter(buf) - for _, item := range source.File { - // The following statements can be simplified as zw.Copy(item) in go1.17 - writer, err := zw.Create(item.Name) + preset := func(filePath string) *bytes.Buffer { + source, err := zip.OpenReader(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) - readerCloser, err := item.Open() + buf := new(bytes.Buffer) + zw := zip.NewWriter(buf) + for _, item := range source.File { + // The following statements can be simplified as zw.Copy(item) in go1.17 + writer, err := zw.Create(item.Name) + assert.NoError(t, err) + readerCloser, err := item.Open() + assert.NoError(t, err) + _, err = io.Copy(writer, readerCloser) + assert.NoError(t, err) + } + fi, err := zw.Create(filePath) assert.NoError(t, err) - _, err = io.Copy(writer, readerCloser) + _, err = fi.Write(MacintoshCyrillicCharset) assert.NoError(t, err) + assert.NoError(t, zw.Close()) + return buf + } + for _, defaultXMLPath := range []string{defaultXMLPathCalcChain, defaultXMLPathStyles} { + _, err = OpenReader(preset(defaultXMLPath)) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } - fi, err := zw.Create(defaultXMLPathCalcChain) - assert.NoError(t, err) - _, err = fi.Write(MacintoshCyrillicCharset) - assert.NoError(t, err) - assert.NoError(t, zw.Close()) - _, err = OpenReader(buf) - assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") // Test open spreadsheet with unzip size limit. _, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipSizeLimit: 100}) @@ -466,29 +471,16 @@ func TestGetCellHyperLink(t *testing.T) { func TestSetSheetBackground(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - err = f.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg")) - if !assert.NoError(t, err) { - t.FailNow() - } - - err = f.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg")) - if !assert.NoError(t, err) { - t.FailNow() - } - + assert.NoError(t, err) + assert.NoError(t, f.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg"))) + assert.NoError(t, f.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetBackground.xlsx"))) assert.NoError(t, f.Close()) } func TestSetSheetBackgroundErrors(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) err = f.SetSheetBackground("Sheet2", filepath.Join("test", "not_exists", "not_exists.png")) if assert.Error(t, err) { @@ -497,7 +489,16 @@ func TestSetSheetBackgroundErrors(t *testing.T) { err = f.SetSheetBackground("Sheet2", filepath.Join("test", "Book1.xlsx")) assert.EqualError(t, err, ErrImgExt.Error()) + // Test set sheet background on not exist worksheet. + err = f.SetSheetBackground("SheetN", filepath.Join("test", "images", "background.jpg")) + assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.Close()) + + // Test set sheet background with unsupported charset content types. + f = NewFile() + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetSheetBackground("Sheet1", filepath.Join("test", "images", "background.jpg")), "XML syntax error on line 1: invalid UTF-8") } // TestWriteArrayFormula tests the extended options of SetCellFormula by writing an array function @@ -1027,12 +1028,6 @@ func TestGetSheetComments(t *testing.T) { assert.Equal(t, "", f.getSheetComments("sheet0")) } -func TestSetSheetVisible(t *testing.T) { - f := NewFile() - f.WorkBook.Sheets.Sheet[0].Name = "SheetN" - assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "sheet SheetN does not exist") -} - func TestGetActiveSheetIndex(t *testing.T) { f := NewFile() f.WorkBook.BookViews = nil @@ -1334,6 +1329,10 @@ func TestAddVBAProject(t *testing.T) { // Test add VBA project twice. assert.NoError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddVBAProject.xlsm"))) + // Test add VBs with unsupported charset workbook relationships. + f.Relationships.Delete(defaultXMLPathWorkbookRels) + f.Pkg.Store(defaultXMLPathWorkbookRels, MacintoshCyrillicCharset) + assert.EqualError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin")), "XML syntax error on line 1: invalid UTF-8") } func TestContentTypesReader(t *testing.T) { @@ -1341,7 +1340,8 @@ func TestContentTypesReader(t *testing.T) { f := NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - f.contentTypesReader() + _, err := f.contentTypesReader() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestWorkbookReader(t *testing.T) { @@ -1349,7 +1349,8 @@ func TestWorkbookReader(t *testing.T) { f := NewFile() f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) - f.workbookReader() + _, err := f.workbookReader() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestWorkSheetReader(t *testing.T) { @@ -1373,19 +1374,28 @@ func TestWorkSheetReader(t *testing.T) { func TestRelsReader(t *testing.T) { // Test unsupported charset. f := NewFile() - rels := "xl/_rels/workbook.xml.rels" + rels := defaultXMLPathWorkbookRels f.Relationships.Store(rels, nil) f.Pkg.Store(rels, MacintoshCyrillicCharset) - f.relsReader(rels) + _, err := f.relsReader(rels) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestDeleteSheetFromWorkbookRels(t *testing.T) { f := NewFile() - rels := "xl/_rels/workbook.xml.rels" + rels := defaultXMLPathWorkbookRels f.Relationships.Store(rels, nil) assert.Equal(t, f.deleteSheetFromWorkbookRels("rID"), "") } +func TestUpdateLinkedValue(t *testing.T) { + f := NewFile() + // Test update lined value with unsupported charset workbook. + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.UpdateLinkedValue(), "XML syntax error on line 1: invalid UTF-8") +} + func TestAttrValToInt(t *testing.T) { _, err := attrValToInt("r", []xml.Attr{ {Name: xml.Name{Local: "r"}, Value: "s"}, diff --git a/file.go b/file.go index 31eaa3d301..30ae506f0e 100644 --- a/file.go +++ b/file.go @@ -30,7 +30,7 @@ func NewFile() *File { f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels)) f.Pkg.Store(defaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp)) f.Pkg.Store(defaultXMLPathDocPropsCore, []byte(xml.Header+templateDocpropsCore)) - f.Pkg.Store("xl/_rels/workbook.xml.rels", []byte(xml.Header+templateWorkbookRels)) + f.Pkg.Store(defaultXMLPathWorkbookRels, []byte(xml.Header+templateWorkbookRels)) f.Pkg.Store("xl/theme/theme1.xml", []byte(xml.Header+templateTheme)) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+templateSheet)) f.Pkg.Store(defaultXMLPathStyles, []byte(xml.Header+templateStyles)) @@ -39,18 +39,19 @@ func NewFile() *File { f.SheetCount = 1 f.CalcChain, _ = f.calcChainReader() f.Comments = make(map[string]*xlsxComments) - f.ContentTypes = f.contentTypesReader() + f.ContentTypes, _ = f.contentTypesReader() f.Drawings = sync.Map{} f.Styles, _ = f.stylesReader() f.DecodeVMLDrawing = make(map[string]*decodeVmlDrawing) f.VMLDrawing = make(map[string]*vmlDrawing) - f.WorkBook = f.workbookReader() + f.WorkBook, _ = f.workbookReader() f.Relationships = sync.Map{} - f.Relationships.Store("xl/_rels/workbook.xml.rels", f.relsReader("xl/_rels/workbook.xml.rels")) + rels, _ := f.relsReader(defaultXMLPathWorkbookRels) + f.Relationships.Store(defaultXMLPathWorkbookRels, rels) f.sheetMap["Sheet1"] = "xl/worksheets/sheet1.xml" ws, _ := f.workSheetReader("Sheet1") f.Sheet.Store("xl/worksheets/sheet1.xml", ws) - f.Theme = f.themeReader() + f.Theme, _ = f.themeReader() return f } @@ -119,7 +120,9 @@ func (f *File) WriteTo(w io.Writer, opts ...Options) (int64, error) { if !ok { return 0, ErrWorkbookFileFormat } - f.setContentTypePartProjectExtensions(contentType) + if err := f.setContentTypePartProjectExtensions(contentType); err != nil { + return 0, err + } } if f.options != nil && f.options.Password != "" { buf, err := f.WriteToBuffer() diff --git a/file_test.go b/file_test.go index 83a9b786f6..4272a7b4f1 100644 --- a/file_test.go +++ b/file_test.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "os" + "path/filepath" "strings" "sync" "testing" @@ -79,6 +80,14 @@ func TestWriteTo(t *testing.T) { _, err := f.WriteTo(bufio.NewWriter(&buf)) assert.EqualError(t, err, ErrWorkbookFileFormat.Error()) } + // Test write with unsupported charset content types. + { + f, buf := NewFile(), bytes.Buffer{} + f.ContentTypes, f.Path = nil, filepath.Join("test", "TestWriteTo.xlsx") + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + _, err := f.WriteTo(bufio.NewWriter(&buf)) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + } } func TestClose(t *testing.T) { diff --git a/picture.go b/picture.go index 6cf1104c53..4e8a652a25 100644 --- a/picture.go +++ b/picture.go @@ -187,7 +187,9 @@ func (f *File) AddPictureFromBytes(sheet, cell, opts, name, extension string, fi if err != nil { return err } - f.addContentTypePart(drawingID, "drawings") + if err = f.addContentTypePart(drawingID, "drawings"); err != nil { + return err + } f.addSheetNameSpace(sheet, SourceRelationship) return err } @@ -201,7 +203,7 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { name = strings.ToLower(sheet) + ".xml" } rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - sheetRels := f.relsReader(rels) + sheetRels, _ := f.relsReader(rels) if sheetRels == nil { sheetRels = &xlsxRelationships{} } @@ -235,11 +237,15 @@ func (f *File) addSheetDrawing(sheet string, rID int) { // addSheetPicture provides a function to add picture element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. -func (f *File) addSheetPicture(sheet string, rID int) { - ws, _ := f.workSheetReader(sheet) +func (f *File) addSheetPicture(sheet string, rID int) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } ws.Picture = &xlsxPicture{ RID: "rId" + strconv.Itoa(rID), } + return err } // countDrawings provides a function to get drawing files count storage in the @@ -378,12 +384,15 @@ func (f *File) addMedia(file []byte, ext string) string { // setContentTypePartImageExtensions provides a function to set the content // type for relationship parts and the Main Document part. -func (f *File) setContentTypePartImageExtensions() { +func (f *File) setContentTypePartImageExtensions() error { imageTypes := map[string]string{ "jpeg": "image/", "png": "image/", "gif": "image/", "svg": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-", "emz": "image/x-", "wmz": "image/x-", } - content := f.contentTypesReader() + content, err := f.contentTypesReader() + if err != nil { + return err + } content.Lock() defer content.Unlock() for _, file := range content.Defaults { @@ -395,13 +404,17 @@ func (f *File) setContentTypePartImageExtensions() { ContentType: prefix + extension, }) } + return err } // setContentTypePartVMLExtensions provides a function to set the content type // for relationship parts and the Main Document part. -func (f *File) setContentTypePartVMLExtensions() { - vml := false - content := f.contentTypesReader() +func (f *File) setContentTypePartVMLExtensions() error { + var vml bool + content, err := f.contentTypesReader() + if err != nil { + return err + } content.Lock() defer content.Unlock() for _, v := range content.Defaults { @@ -415,12 +428,13 @@ func (f *File) setContentTypePartVMLExtensions() { ContentType: ContentTypeVML, }) } + return err } // addContentTypePart provides a function to add content type part // relationships in the file [Content_Types].xml by given index. -func (f *File) addContentTypePart(index int, contentType string) { - setContentType := map[string]func(){ +func (f *File) addContentTypePart(index int, contentType string) error { + setContentType := map[string]func() error{ "comments": f.setContentTypePartVMLExtensions, "drawings": f.setContentTypePartImageExtensions, } @@ -446,20 +460,26 @@ func (f *File) addContentTypePart(index int, contentType string) { } s, ok := setContentType[contentType] if ok { - s() + if err := s(); err != nil { + return err + } + } + content, err := f.contentTypesReader() + if err != nil { + return err } - content := f.contentTypesReader() content.Lock() defer content.Unlock() for _, v := range content.Overrides { if v.PartName == partNames[contentType] { - return + return err } } content.Overrides = append(content.Overrides, xlsxOverride{ PartName: partNames[contentType], ContentType: contentTypes[contentType], }) + return err } // getSheetRelationshipsTargetByID provides a function to get Target attribute @@ -471,7 +491,7 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { name = strings.ToLower(sheet) + ".xml" } rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - sheetRels := f.relsReader(rels) + sheetRels, _ := f.relsReader(rels) if sheetRels == nil { sheetRels = &xlsxRelationships{} } @@ -630,7 +650,7 @@ func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsD // from xl/drawings/_rels/drawing%s.xml.rels by given file name and // relationship ID. func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship { - if drawingRels := f.relsReader(rels); drawingRels != nil { + if drawingRels, _ := f.relsReader(rels); drawingRels != nil { drawingRels.Lock() defer drawingRels.Unlock() for _, v := range drawingRels.Relationships { diff --git a/picture_test.go b/picture_test.go index 65abf9ed8e..11196c6642 100644 --- a/picture_test.go +++ b/picture_test.go @@ -67,6 +67,12 @@ func TestAddPicture(t *testing.T) { // Test write file to given path. assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx"))) assert.NoError(t, f.Close()) + + // Test add picture with unsupported charset content types. + f = NewFile() + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file), "XML syntax error on line 1: invalid UTF-8") } func TestAddPictureErrors(t *testing.T) { @@ -236,3 +242,27 @@ func TestDrawingResize(t *testing.T) { ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } + +func TestSetContentTypePartImageExtensions(t *testing.T) { + f := NewFile() + // Test set content type part image extensions with unsupported charset content types. + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.setContentTypePartImageExtensions(), "XML syntax error on line 1: invalid UTF-8") +} + +func TestSetContentTypePartVMLExtensions(t *testing.T) { + f := NewFile() + // Test set content type part VML extensions with unsupported charset content types. + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.setContentTypePartVMLExtensions(), "XML syntax error on line 1: invalid UTF-8") +} + +func TestAddContentTypePart(t *testing.T) { + f := NewFile() + // Test add content type part with unsupported charset content types. + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.addContentTypePart(0, "unknown"), "XML syntax error on line 1: invalid UTF-8") +} diff --git a/pivotTable.go b/pivotTable.go index 0999a97162..7b4b5535b4 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -160,10 +160,10 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) error { } pivotTableSheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(pivotTableSheetPath, "xl/worksheets/") + ".rels" f.addRels(pivotTableSheetRels, SourceRelationshipPivotTable, sheetRelationshipsPivotTableXML, "") - f.addContentTypePart(pivotTableID, "pivotTable") - f.addContentTypePart(pivotCacheID, "pivotCache") - - return nil + if err = f.addContentTypePart(pivotTableID, "pivotTable"); err != nil { + return err + } + return f.addContentTypePart(pivotCacheID, "pivotCache") } // parseFormatPivotTableSet provides a function to validate pivot table @@ -697,7 +697,7 @@ func (f *File) getPivotTableFieldOptions(name string, fields []PivotTableField) // addWorkbookPivotCache add the association ID of the pivot cache in workbook.xml. func (f *File) addWorkbookPivotCache(RID int) int { - wb := f.workbookReader() + wb, _ := f.workbookReader() if wb.PivotCaches == nil { wb.PivotCaches = &xlsxPivotCaches{} } diff --git a/pivotTable_test.go b/pivotTable_test.go index 5d2e537853..fc9e09063d 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -259,6 +259,15 @@ func TestAddPivotTable(t *testing.T) { // Test get pivot fields index with empty data range _, err = f.getPivotFieldsIndex([]PivotTableField{}, &PivotTableOptions{}) assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) + // Test add pivot table with unsupported charset content types. + f = NewFile() + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$G$2:$M$34", + Rows: []PivotTableField{{Data: "Year"}}, + }), "XML syntax error on line 1: invalid UTF-8") } func TestAddPivotRowFields(t *testing.T) { diff --git a/rows.go b/rows.go index 34a227f8e9..9f1ac73d57 100644 --- a/rows.go +++ b/rows.go @@ -435,8 +435,13 @@ func (f *File) sharedStringsReader() (*xlsxSST, error) { f.sharedStringsMap[sharedStrings.SI[i].T.Val] = i } } - f.addContentTypePart(0, "sharedStrings") - rels := f.relsReader(relPath) + if err = f.addContentTypePart(0, "sharedStrings"); err != nil { + return f.SharedStrings, err + } + rels, err := f.relsReader(relPath) + if err != nil { + return f.SharedStrings, err + } for _, rel := range rels.Relationships { if rel.Target == "/xl/sharedStrings.xml" { return f.SharedStrings, nil diff --git a/rows_test.go b/rows_test.go index 5317c222de..2e49c2877b 100644 --- a/rows_test.go +++ b/rows_test.go @@ -235,9 +235,20 @@ func TestSharedStringsReader(t *testing.T) { f := NewFile() // Test read shared string with unsupported charset. f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) - f.sharedStringsReader() - si := xlsxSI{} - assert.EqualValues(t, "", si.String()) + _, err := f.sharedStringsReader() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test read shared strings with unsupported charset content types. + f = NewFile() + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + _, err = f.sharedStringsReader() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test read shared strings with unsupported charset workbook relationships. + f = NewFile() + f.Relationships.Delete(defaultXMLPathWorkbookRels) + f.Pkg.Store(defaultXMLPathWorkbookRels, MacintoshCyrillicCharset) + _, err = f.sharedStringsReader() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestRowVisibility(t *testing.T) { diff --git a/shape.go b/shape.go index 6f7c8fd4d5..2022ee6775 100644 --- a/shape.go +++ b/shape.go @@ -308,8 +308,7 @@ func (f *File) AddShape(sheet, cell, opts string) error { if err = f.addDrawingShape(sheet, drawingXML, cell, options); err != nil { return err } - f.addContentTypePart(drawingID, "drawings") - return err + return f.addContentTypePart(drawingID, "drawings") } // addDrawingShape provides a function to add preset geometry by given sheet, diff --git a/shape_test.go b/shape_test.go index 9d1da8a07f..2b2e87cb87 100644 --- a/shape_test.go +++ b/shape_test.go @@ -87,10 +87,15 @@ func TestAddShape(t *testing.T) { } }`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) - // Test set row style with unsupported charset style sheet. + // Test add shape with unsupported charset style sheet. f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) assert.EqualError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`), "XML syntax error on line 1: invalid UTF-8") + // Test add shape with unsupported charset content types. + f = NewFile() + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`), "XML syntax error on line 1: invalid UTF-8") } func TestAddDrawingShape(t *testing.T) { diff --git a/sheet.go b/sheet.go index 0616d95b3c..b9de81c9e9 100644 --- a/sheet.go +++ b/sheet.go @@ -17,7 +17,6 @@ import ( "encoding/xml" "fmt" "io" - "log" "os" "path" "path/filepath" @@ -47,7 +46,7 @@ func (f *File) NewSheet(sheet string) int { } f.DeleteSheet(sheet) f.SheetCount++ - wb := f.workbookReader() + wb, _ := f.workbookReader() sheetID := 0 for _, v := range wb.Sheets.Sheet { if v.SheetID > sheetID { @@ -56,7 +55,7 @@ func (f *File) NewSheet(sheet string) int { } sheetID++ // Update [Content_Types].xml - f.setContentTypes("/xl/worksheets/sheet"+strconv.Itoa(sheetID)+".xml", ContentTypeSpreadSheetMLWorksheet) + _ = f.setContentTypes("/xl/worksheets/sheet"+strconv.Itoa(sheetID)+".xml", ContentTypeSpreadSheetMLWorksheet) // Create new sheet /xl/worksheets/sheet%d.xml f.setSheet(sheetID, sheet) // Update workbook.xml.rels @@ -68,19 +67,17 @@ func (f *File) NewSheet(sheet string) int { // contentTypesReader provides a function to get the pointer to the // [Content_Types].xml structure after deserialization. -func (f *File) contentTypesReader() *xlsxTypes { - var err error - +func (f *File) contentTypesReader() (*xlsxTypes, error) { if f.ContentTypes == nil { f.ContentTypes = new(xlsxTypes) f.ContentTypes.Lock() defer f.ContentTypes.Unlock() - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathContentTypes)))). + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathContentTypes)))). Decode(f.ContentTypes); err != nil && err != io.EOF { - log.Printf("xml decode error: %s", err) + return f.ContentTypes, err } } - return f.ContentTypes + return f.ContentTypes, nil } // contentTypesWriter provides a function to save [Content_Types].xml after @@ -215,14 +212,18 @@ func trimCell(column []xlsxC) []xlsxC { // setContentTypes provides a function to read and update property of contents // type of the spreadsheet. -func (f *File) setContentTypes(partName, contentType string) { - content := f.contentTypesReader() +func (f *File) setContentTypes(partName, contentType string) error { + content, err := f.contentTypesReader() + if err != nil { + return err + } content.Lock() defer content.Unlock() content.Overrides = append(content.Overrides, xlsxOverride{ PartName: partName, ContentType: contentType, }) + return err } // setSheet provides a function to update sheet property by given index. @@ -271,7 +272,7 @@ func (f *File) SetActiveSheet(index int) { if index < 0 { index = 0 } - wb := f.workbookReader() + wb, _ := f.workbookReader() for activeTab := range wb.Sheets.Sheet { if activeTab == index { if wb.BookViews == nil { @@ -316,7 +317,7 @@ func (f *File) SetActiveSheet(index int) { // spreadsheet. If not found the active sheet will be return integer 0. func (f *File) GetActiveSheetIndex() (index int) { sheetID := f.getActiveSheetID() - wb := f.workbookReader() + wb, _ := f.workbookReader() if wb != nil { for idx, sheet := range wb.Sheets.Sheet { if sheet.SheetID == sheetID { @@ -331,7 +332,7 @@ func (f *File) GetActiveSheetIndex() (index int) { // getActiveSheetID provides a function to get active sheet ID of the // spreadsheet. If not found the active sheet will be return integer 0. func (f *File) getActiveSheetID() int { - wb := f.workbookReader() + wb, _ := f.workbookReader() if wb != nil { if wb.BookViews != nil && len(wb.BookViews.WorkBookView) > 0 { activeTab := wb.BookViews.WorkBookView[0].ActiveTab @@ -357,10 +358,10 @@ func (f *File) SetSheetName(source, target string) { if strings.EqualFold(target, source) { return } - content := f.workbookReader() - for k, v := range content.Sheets.Sheet { + wb, _ := f.workbookReader() + for k, v := range wb.Sheets.Sheet { if v.Name == source { - content.Sheets.Sheet[k].Name = target + wb.Sheets.Sheet[k].Name = target f.sheetMap[target] = f.sheetMap[source] delete(f.sheetMap, source) } @@ -422,7 +423,7 @@ func (f *File) GetSheetIndex(sheet string) int { // fmt.Println(index, name) // } func (f *File) GetSheetMap() map[int]string { - wb := f.workbookReader() + wb, _ := f.workbookReader() sheetMap := map[int]string{} if wb != nil { for _, sheet := range wb.Sheets.Sheet { @@ -435,7 +436,7 @@ func (f *File) GetSheetMap() map[int]string { // GetSheetList provides a function to get worksheets, chart sheets, and // dialog sheets name list of the workbook. func (f *File) GetSheetList() (list []string) { - wb := f.workbookReader() + wb, _ := f.workbookReader() if wb != nil { for _, sheet := range wb.Sheets.Sheet { list = append(list, sheet.Name) @@ -448,8 +449,10 @@ func (f *File) GetSheetList() (list []string) { // of the spreadsheet. func (f *File) getSheetMap() map[string]string { maps := map[string]string{} - for _, v := range f.workbookReader().Sheets.Sheet { - for _, rel := range f.relsReader(f.getWorkbookRelsPath()).Relationships { + wb, _ := f.workbookReader() + rels, _ := f.relsReader(f.getWorkbookRelsPath()) + for _, v := range wb.Sheets.Sheet { + for _, rel := range rels.Relationships { if rel.ID == v.ID { sheetXMLPath := f.getWorksheetPath(rel.Target) if _, ok := f.Pkg.Load(sheetXMLPath); ok { @@ -498,10 +501,11 @@ func (f *File) SetSheetBackground(sheet, picture string) error { sheetXMLPath, _ := f.getSheetXMLPath(sheet) sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") - f.addSheetPicture(sheet, rID) + if err = f.addSheetPicture(sheet, rID); err != nil { + return err + } f.addSheetNameSpace(sheet, SourceRelationship) - f.setContentTypePartImageExtensions() - return err + return f.setContentTypePartImageExtensions() } // DeleteSheet provides a function to delete worksheet in a workbook by given @@ -514,8 +518,8 @@ func (f *File) DeleteSheet(sheet string) { return } sheetName := trimSheetName(sheet) - wb := f.workbookReader() - wbRels := f.relsReader(f.getWorkbookRelsPath()) + wb, _ := f.workbookReader() + wbRels, _ := f.relsReader(f.getWorkbookRelsPath()) activeSheetName := f.GetSheetName(f.GetActiveSheetIndex()) deleteLocalSheetID := f.GetSheetIndex(sheet) deleteAndAdjustDefinedNames(wb, deleteLocalSheetID) @@ -537,8 +541,8 @@ func (f *File) DeleteSheet(sheet string) { } } target := f.deleteSheetFromWorkbookRels(v.ID) - f.deleteSheetFromContentTypes(target) - f.deleteCalcChain(f.getSheetID(sheet), "") + _ = f.deleteSheetFromContentTypes(target) + _ = f.deleteCalcChain(f.getSheetID(sheet), "") delete(f.sheetMap, v.Name) f.Pkg.Delete(sheetXML) f.Pkg.Delete(rels) @@ -573,12 +577,12 @@ func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) { // deleteSheetFromWorkbookRels provides a function to remove worksheet // relationships by given relationships ID in the file workbook.xml.rels. func (f *File) deleteSheetFromWorkbookRels(rID string) string { - content := f.relsReader(f.getWorkbookRelsPath()) - content.Lock() - defer content.Unlock() - for k, v := range content.Relationships { + rels, _ := f.relsReader(f.getWorkbookRelsPath()) + rels.Lock() + defer rels.Unlock() + for k, v := range rels.Relationships { if v.ID == rID { - content.Relationships = append(content.Relationships[:k], content.Relationships[k+1:]...) + rels.Relationships = append(rels.Relationships[:k], rels.Relationships[k+1:]...) return v.Target } } @@ -587,11 +591,14 @@ func (f *File) deleteSheetFromWorkbookRels(rID string) string { // deleteSheetFromContentTypes provides a function to remove worksheet // relationships by given target name in the file [Content_Types].xml. -func (f *File) deleteSheetFromContentTypes(target string) { +func (f *File) deleteSheetFromContentTypes(target string) error { if !strings.HasPrefix(target, "/") { target = "/xl/" + target } - content := f.contentTypesReader() + content, err := f.contentTypesReader() + if err != nil { + return err + } content.Lock() defer content.Unlock() for k, v := range content.Overrides { @@ -599,6 +606,7 @@ func (f *File) deleteSheetFromContentTypes(target string) { content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...) } } + return err } // CopySheet provides a function to duplicate a worksheet by gave source and @@ -659,22 +667,25 @@ func (f *File) copySheet(from, to int) error { // err := f.SetSheetVisible("Sheet1", false) func (f *File) SetSheetVisible(sheet string, visible bool) error { sheet = trimSheetName(sheet) - content := f.workbookReader() + wb, err := f.workbookReader() + if err != nil { + return err + } if visible { - for k, v := range content.Sheets.Sheet { + for k, v := range wb.Sheets.Sheet { if strings.EqualFold(v.Name, sheet) { - content.Sheets.Sheet[k].State = "" + wb.Sheets.Sheet[k].State = "" } } - return nil + return err } count := 0 - for _, v := range content.Sheets.Sheet { + for _, v := range wb.Sheets.Sheet { if v.State != "hidden" { count++ } } - for k, v := range content.Sheets.Sheet { + for k, v := range wb.Sheets.Sheet { ws, err := f.workSheetReader(v.Name) if err != nil { return err @@ -684,10 +695,10 @@ func (f *File) SetSheetVisible(sheet string, visible bool) error { tabSelected = ws.SheetViews.SheetView[0].TabSelected } if strings.EqualFold(v.Name, sheet) && count > 1 && !tabSelected { - content.Sheets.Sheet[k].State = "hidden" + wb.Sheets.Sheet[k].State = "hidden" } } - return nil + return err } // parsePanesOptions provides a function to parse the panes settings. @@ -830,10 +841,11 @@ func (f *File) SetPanes(sheet, panes string) error { // // f.GetSheetVisible("Sheet1") func (f *File) GetSheetVisible(sheet string) bool { - content, name, visible := f.workbookReader(), trimSheetName(sheet), false - for k, v := range content.Sheets.Sheet { + name, visible := trimSheetName(sheet), false + wb, _ := f.workbookReader() + for k, v := range wb.Sheets.Sheet { if strings.EqualFold(v.Name, name) { - if content.Sheets.Sheet[k].State == "" || content.Sheets.Sheet[k].State == "visible" { + if wb.Sheets.Sheet[k].State == "" || wb.Sheets.Sheet[k].State == "visible" { visible = true } } @@ -1460,7 +1472,10 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) { // Scope: "Sheet2", // }) func (f *File) SetDefinedName(definedName *DefinedName) error { - wb := f.workbookReader() + wb, err := f.workbookReader() + if err != nil { + return err + } d := xlsxDefinedName{ Name: definedName.Name, Comment: definedName.Comment, @@ -1499,7 +1514,10 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { // Scope: "Sheet2", // }) func (f *File) DeleteDefinedName(definedName *DefinedName) error { - wb := f.workbookReader() + wb, err := f.workbookReader() + if err != nil { + return err + } if wb.DefinedNames != nil { for idx, dn := range wb.DefinedNames.DefinedName { scope := "Workbook" @@ -1512,7 +1530,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error { } if scope == deleteScope && dn.Name == definedName.Name { wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...) - return nil + return err } } } @@ -1523,7 +1541,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error { // or worksheet. func (f *File) GetDefinedName() []DefinedName { var definedNames []DefinedName - wb := f.workbookReader() + wb, _ := f.workbookReader() if wb.DefinedNames != nil { for _, dn := range wb.DefinedNames.DefinedName { definedName := DefinedName{ @@ -1715,23 +1733,22 @@ func (f *File) RemovePageBreak(sheet, cell string) error { // relsReader provides a function to get the pointer to the structure // after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. -func (f *File) relsReader(path string) *xlsxRelationships { - var err error +func (f *File) relsReader(path string) (*xlsxRelationships, error) { rels, _ := f.Relationships.Load(path) if rels == nil { if _, ok := f.Pkg.Load(path); ok { c := xlsxRelationships{} - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). Decode(&c); err != nil && err != io.EOF { - log.Printf("xml decode error: %s", err) + return nil, err } f.Relationships.Store(path, &c) } } if rels, _ = f.Relationships.Load(path); rels != nil { - return rels.(*xlsxRelationships) + return rels.(*xlsxRelationships), nil } - return nil + return nil, nil } // fillSheetData ensures there are enough rows, and columns in the chosen diff --git a/sheet_test.go b/sheet_test.go index 4b9d31eccc..08c7c1a9ea 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -188,6 +188,17 @@ func TestDefinedName(t *testing.T) { assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[0].RefersTo) assert.Exactly(t, 1, len(f.GetDefinedName())) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx"))) + // Test set defined name with unsupported charset workbook. + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetDefinedName(&DefinedName{ + Name: "Amount", RefersTo: "Sheet1!$A$2:$D$5", + }), "XML syntax error on line 1: invalid UTF-8") + // Test delete defined name with unsupported charset workbook. + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.DeleteDefinedName(&DefinedName{Name: "Amount"}), + "XML syntax error on line 1: invalid UTF-8") } func TestGroupSheets(t *testing.T) { @@ -367,6 +378,32 @@ func TestGetSheetID(t *testing.T) { assert.NotEqual(t, -1, id) } +func TestSetSheetVisible(t *testing.T) { + f := NewFile() + f.WorkBook.Sheets.Sheet[0].Name = "SheetN" + assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "sheet SheetN does not exist") + // Test set sheet visible with unsupported charset workbook. + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "XML syntax error on line 1: invalid UTF-8") +} + +func TestSetContentTypes(t *testing.T) { + f := NewFile() + // Test set content type with unsupported charset content types. + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.setContentTypes("/xl/worksheets/sheet1.xml", ContentTypeSpreadSheetMLWorksheet), "XML syntax error on line 1: invalid UTF-8") +} + +func TestDeleteSheetFromContentTypes(t *testing.T) { + f := NewFile() + // Test delete sheet from content types with unsupported charset content types. + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteSheetFromContentTypes("/xl/worksheets/sheet1.xml"), "XML syntax error on line 1: invalid UTF-8") +} + func BenchmarkNewSheet(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { diff --git a/stream.go b/stream.go index 766e83a47c..d348ebaad5 100644 --- a/stream.go +++ b/stream.go @@ -226,11 +226,12 @@ func (sw *StreamWriter) AddTable(hCell, vCell, opts string) error { sw.tableParts = fmt.Sprintf(``, rID) - sw.File.addContentTypePart(tableID, "table") - + if err = sw.File.addContentTypePart(tableID, "table"); err != nil { + return err + } b, _ := xml.Marshal(table) sw.File.saveFileList(tableXML, b) - return nil + return err } // Extract values from a row in the StreamWriter. @@ -471,6 +472,23 @@ func setCellFormula(c *xlsxC, formula string) { } } +// setCellTime provides a function to set number of a cell with a time. +func (sw *StreamWriter) setCellTime(c *xlsxC, val time.Time) error { + var date1904, isNum bool + wb, err := sw.File.workbookReader() + if err != nil { + return err + } + if wb != nil && wb.WorkbookPr != nil { + date1904 = wb.WorkbookPr.Date1904 + } + if isNum, err = c.setCellTime(val, date1904); err == nil && isNum && c.S == 0 { + style, _ := sw.File.NewStyle(&Style{NumFmt: 22}) + c.S = style + } + return nil +} + // setCellValFunc provides a function to set value of a cell. func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error { var err error @@ -488,15 +506,7 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error { case time.Duration: c.T, c.V = setCellDuration(val) case time.Time: - var isNum bool - date1904, wb := false, sw.File.workbookReader() - if wb != nil && wb.WorkbookPr != nil { - date1904 = wb.WorkbookPr.Date1904 - } - if isNum, err = c.setCellTime(val, date1904); isNum && c.S == 0 { - style, _ := sw.File.NewStyle(&Style{NumFmt: 22}) - c.S = style - } + err = sw.setCellTime(c, val) case bool: c.T, c.V = setCellBool(val) case nil: diff --git a/stream_test.go b/stream_test.go index 65af283eab..dca06aaff7 100644 --- a/stream_test.go +++ b/stream_test.go @@ -186,7 +186,7 @@ func TestStreamTable(t *testing.T) { } // Write a table. - assert.NoError(t, streamWriter.AddTable("A1", "C2", ``)) + assert.NoError(t, streamWriter.AddTable("A1", "C2", "")) assert.NoError(t, streamWriter.Flush()) // Verify the table has names. @@ -198,13 +198,17 @@ func TestStreamTable(t *testing.T) { assert.Equal(t, "B", table.TableColumns.TableColumn[1].Name) assert.Equal(t, "C", table.TableColumns.TableColumn[2].Name) - assert.NoError(t, streamWriter.AddTable("A1", "C1", ``)) + assert.NoError(t, streamWriter.AddTable("A1", "C1", "")) // Test add table with illegal options. assert.EqualError(t, streamWriter.AddTable("B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") // Test add table with illegal cell reference. assert.EqualError(t, streamWriter.AddTable("A", "B1", `{}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, streamWriter.AddTable("A1", "B", `{}`), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + // Test add table with unsupported charset content types. + file.ContentTypes = nil + file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, streamWriter.AddTable("A1", "C2", ""), "XML syntax error on line 1: invalid UTF-8") } func TestStreamMergeCells(t *testing.T) { @@ -242,7 +246,7 @@ func TestStreamMarshalAttrs(t *testing.T) { } func TestStreamSetRow(t *testing.T) { - // Test error exceptions + // Test error exceptions. file := NewFile() defer func() { assert.NoError(t, file.Close()) @@ -250,9 +254,13 @@ func TestStreamSetRow(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - // Test set row with non-ascending row number + // Test set row with non-ascending row number. assert.NoError(t, streamWriter.SetRow("A1", []interface{}{})) assert.EqualError(t, streamWriter.SetRow("A1", []interface{}{}), newStreamSetRowError(1).Error()) + // Test set row with unsupported charset workbook. + file.WorkBook = nil + file.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, streamWriter.SetRow("A2", []interface{}{time.Now()}), "XML syntax error on line 1: invalid UTF-8") } func TestStreamSetRowNilValues(t *testing.T) { diff --git a/styles.go b/styles.go index 08d6b0c947..79bd6d3622 100644 --- a/styles.go +++ b/styles.go @@ -17,7 +17,6 @@ import ( "encoding/xml" "fmt" "io" - "log" "math" "reflect" "strconv" @@ -3357,16 +3356,16 @@ func getPaletteColor(color string) string { // themeReader provides a function to get the pointer to the xl/theme/theme1.xml // structure after deserialization. -func (f *File) themeReader() *xlsxTheme { +func (f *File) themeReader() (*xlsxTheme, error) { if _, ok := f.Pkg.Load(defaultXMLPathTheme); !ok { - return nil + return nil, nil } theme := xlsxTheme{XMLNSa: NameSpaceDrawingML.Value, XMLNSr: SourceRelationship.Value} if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathTheme)))). Decode(&theme); err != nil && err != io.EOF { - log.Printf("xml decoder error: %s", err) + return &theme, err } - return &theme + return &theme, nil } // ThemeColor applied the color with tint value. diff --git a/styles_test.go b/styles_test.go index 605ad07e4d..9001d5bef6 100644 --- a/styles_test.go +++ b/styles_test.go @@ -366,7 +366,9 @@ func TestThemeReader(t *testing.T) { f := NewFile() // Test read theme with unsupported charset. f.Pkg.Store(defaultXMLPathTheme, MacintoshCyrillicCharset) - assert.EqualValues(t, &xlsxTheme{XMLNSa: NameSpaceDrawingML.Value, XMLNSr: SourceRelationship.Value}, f.themeReader()) + theme, err := f.themeReader() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.EqualValues(t, &xlsxTheme{XMLNSa: NameSpaceDrawingML.Value, XMLNSr: SourceRelationship.Value}, theme) } func TestSetCellStyle(t *testing.T) { diff --git a/table.go b/table.go index 867af9e24e..06336ff337 100644 --- a/table.go +++ b/table.go @@ -94,8 +94,7 @@ func (f *File) AddTable(sheet, hCell, vCell, opts string) error { if err = f.addTable(sheet, tableXML, hCol, hRow, vCol, vRow, tableID, options); err != nil { return err } - f.addContentTypePart(tableID, "table") - return err + return f.addContentTypePart(tableID, "table") } // countTables provides a function to get table files count storage in the @@ -301,7 +300,10 @@ func (f *File) AutoFilter(sheet, hCell, vCell, opts string) error { cellStart, _ := CoordinatesToCellName(hCol, hRow, true) cellEnd, _ := CoordinatesToCellName(vCol, vRow, true) ref, filterDB := cellStart+":"+cellEnd, "_xlnm._FilterDatabase" - wb := f.workbookReader() + wb, err := f.workbookReader() + if err != nil { + return err + } sheetID := f.GetSheetIndex(sheet) filterRange := fmt.Sprintf("'%s'!%s", sheet, ref) d := xlsxDefinedName{ diff --git a/table_test.go b/table_test.go index 409b49fb5d..5ac464b19e 100644 --- a/table_test.go +++ b/table_test.go @@ -78,9 +78,13 @@ func TestAutoFilter(t *testing.T) { }) } - // Test AutoFilter with illegal cell reference. + // Test add auto filter with illegal cell reference. assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + // Test add auto filter with unsupported charset workbook. + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.AutoFilter("Sheet1", "D4", "B1", formats[0]), "XML syntax error on line 1: invalid UTF-8") } func TestAutoFilterError(t *testing.T) { diff --git a/templates.go b/templates.go index c8233c1830..2e0c051903 100644 --- a/templates.go +++ b/templates.go @@ -23,6 +23,7 @@ const ( defaultXMLPathStyles = "xl/styles.xml" defaultXMLPathTheme = "xl/theme/theme1.xml" defaultXMLPathWorkbook = "xl/workbook.xml" + defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" defaultTempFileSST = "sharedStrings" ) diff --git a/workbook.go b/workbook.go index 937c9caf4e..eb57cb5628 100644 --- a/workbook.go +++ b/workbook.go @@ -15,7 +15,6 @@ import ( "bytes" "encoding/xml" "io" - "log" "path/filepath" "strconv" "strings" @@ -23,7 +22,10 @@ import ( // SetWorkbookProps provides a function to sets workbook properties. func (f *File) SetWorkbookProps(opts *WorkbookPropsOptions) error { - wb := f.workbookReader() + wb, err := f.workbookReader() + if err != nil { + return err + } if wb.WorkbookPr == nil { wb.WorkbookPr = new(xlsxWorkbookPr) } @@ -44,20 +46,24 @@ func (f *File) SetWorkbookProps(opts *WorkbookPropsOptions) error { // GetWorkbookProps provides a function to gets workbook properties. func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error) { - wb, opts := f.workbookReader(), WorkbookPropsOptions{} + var opts WorkbookPropsOptions + wb, err := f.workbookReader() + if err != nil { + return opts, err + } if wb.WorkbookPr != nil { opts.Date1904 = boolPtr(wb.WorkbookPr.Date1904) opts.FilterPrivacy = boolPtr(wb.WorkbookPr.FilterPrivacy) opts.CodeName = stringPtr(wb.WorkbookPr.CodeName) } - return opts, nil + return opts, err } // setWorkbook update workbook property of the spreadsheet. Maximum 31 // characters are allowed in sheet title. func (f *File) setWorkbook(name string, sheetID, rid int) { - content := f.workbookReader() - content.Sheets.Sheet = append(content.Sheets.Sheet, xlsxSheet{ + wb, _ := f.workbookReader() + wb.Sheets.Sheet = append(wb.Sheets.Sheet, xlsxSheet{ Name: trimSheetName(name), SheetID: sheetID, ID: "rId" + strconv.Itoa(rid), @@ -67,7 +73,7 @@ func (f *File) setWorkbook(name string, sheetID, rid int) { // getWorkbookPath provides a function to get the path of the workbook.xml in // the spreadsheet. func (f *File) getWorkbookPath() (path string) { - if rels := f.relsReader("_rels/.rels"); rels != nil { + if rels, _ := f.relsReader("_rels/.rels"); rels != nil { rels.Lock() defer rels.Unlock() for _, rel := range rels.Relationships { @@ -95,7 +101,7 @@ func (f *File) getWorkbookRelsPath() (path string) { // workbookReader provides a function to get the pointer to the workbook.xml // structure after deserialization. -func (f *File) workbookReader() *xlsxWorkbook { +func (f *File) workbookReader() (*xlsxWorkbook, error) { var err error if f.WorkBook == nil { wbPath := f.getWorkbookPath() @@ -107,10 +113,10 @@ func (f *File) workbookReader() *xlsxWorkbook { } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))). Decode(f.WorkBook); err != nil && err != io.EOF { - log.Printf("xml decode error: %s", err) + return f.WorkBook, err } } - return f.WorkBook + return f.WorkBook, err } // workBookWriter provides a function to save workbook.xml after serialize diff --git a/workbook_test.go b/workbook_test.go index 29571fab45..a3b2b52672 100644 --- a/workbook_test.go +++ b/workbook_test.go @@ -9,7 +9,8 @@ import ( func TestWorkbookProps(t *testing.T) { f := NewFile() assert.NoError(t, f.SetWorkbookProps(nil)) - wb := f.workbookReader() + wb, err := f.workbookReader() + assert.NoError(t, err) wb.WorkbookPr = nil expected := WorkbookPropsOptions{ Date1904: boolPtr(true), @@ -20,4 +21,13 @@ func TestWorkbookProps(t *testing.T) { opts, err := f.GetWorkbookProps() assert.NoError(t, err) assert.Equal(t, expected, opts) + // Test set workbook properties with unsupported charset workbook. + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetWorkbookProps(&expected), "XML syntax error on line 1: invalid UTF-8") + // Test get workbook properties with unsupported charset workbook. + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + _, err = f.GetWorkbookProps() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } From 45d168c79d2d3f3d0dd6247e2b527f3007d84793 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 15 Nov 2022 22:08:37 +0800 Subject: [PATCH 131/213] This closes #1391, escape XML characters to avoid with corrupt file - Update and improve unit test coverage --- adjust.go | 8 +++----- cell.go | 5 ++++- stream_test.go | 18 +++++++++++++----- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/adjust.go b/adjust.go index bf899274b2..de634fc114 100644 --- a/adjust.go +++ b/adjust.go @@ -279,16 +279,14 @@ func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, off rowData.Hidden = false } } - return nil + return err } coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset) x1, y1, x2, y2 = coordinates[0], coordinates[1], coordinates[2], coordinates[3] - if ws.AutoFilter.Ref, err = f.coordinatesToRangeRef([]int{x1, y1, x2, y2}); err != nil { - return err - } - return nil + ws.AutoFilter.Ref, err = f.coordinatesToRangeRef([]int{x1, y1, x2, y2}) + return err } // adjustAutoFilterHelper provides a function for adjusting auto filter to diff --git a/cell.go b/cell.go index a0a281845d..bbbb83a9ed 100644 --- a/cell.go +++ b/cell.go @@ -12,6 +12,7 @@ package excelize import ( + "bytes" "encoding/xml" "fmt" "os" @@ -490,7 +491,9 @@ func (c *xlsxC) setCellValue(val string) { // string. func (c *xlsxC) setInlineStr(val string) { c.T, c.V, c.IS = "inlineStr", "", &xlsxSI{T: &xlsxT{}} - c.IS.T.Val, c.IS.T.Space = trimCellValue(val) + buf := &bytes.Buffer{} + _ = xml.EscapeText(buf, []byte(val)) + c.IS.T.Val, c.IS.T.Space = trimCellValue(buf.String()) } // setStr set cell data type and value which containing a formula string. diff --git a/stream_test.go b/stream_test.go index dca06aaff7..925a6a789a 100644 --- a/stream_test.go +++ b/stream_test.go @@ -58,11 +58,19 @@ func TestStreamWriter(t *testing.T) { // Test set cell with style and rich text. styleID, err := file.NewStyle(&Style{Font: &Font{Color: "#777777"}}) assert.NoError(t, err) - assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}, Cell{Formula: "SUM(A10,B10)"}}, RowOpts{Height: 45, StyleID: styleID})) - assert.NoError(t, streamWriter.SetRow("A5", []interface{}{&Cell{StyleID: styleID, Value: "cell"}, &Cell{Formula: "SUM(A10,B10)"}, []RichTextRun{ - {Text: "Rich ", Font: &Font{Color: "2354e8"}}, - {Text: "Text", Font: &Font{Color: "e83723"}}, - }})) + assert.NoError(t, streamWriter.SetRow("A4", []interface{}{ + Cell{StyleID: styleID}, + Cell{Formula: "SUM(A10,B10)", Value: " preserve space "}, + }, + RowOpts{Height: 45, StyleID: styleID})) + assert.NoError(t, streamWriter.SetRow("A5", []interface{}{ + &Cell{StyleID: styleID, Value: "cell <>&'\""}, + &Cell{Formula: "SUM(A10,B10)"}, + []RichTextRun{ + {Text: "Rich ", Font: &Font{Color: "2354e8"}}, + {Text: "Text", Font: &Font{Color: "e83723"}}, + }, + })) assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()})) assert.NoError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: 20, Hidden: true, StyleID: styleID})) assert.EqualError(t, streamWriter.SetRow("A8", nil, RowOpts{Height: MaxRowHeight + 1}), ErrMaxRowHeight.Error()) From aa80fa417985cb8f7df77d45825c41a81206df98 Mon Sep 17 00:00:00 2001 From: renxiaotu <35713121+renxiaotu@users.noreply.github.com> Date: Wed, 16 Nov 2022 00:02:35 +0800 Subject: [PATCH 132/213] This made stream writer support set the insert page break (#1393) --- sheet.go | 30 ++++++++++++++++++------------ stream.go | 40 ++++++++++++++++++++++++---------------- stream_test.go | 13 +++++++++++++ xmlWorksheet.go | 16 ++++++++++++++-- 4 files changed, 69 insertions(+), 30 deletions(-) diff --git a/sheet.go b/sheet.go index b9de81c9e9..3ac933bc21 100644 --- a/sheet.go +++ b/sheet.go @@ -1616,19 +1616,25 @@ func (f *File) UngroupSheets() error { } // InsertPageBreak create a page break to determine where the printed page -// ends and where begins the next one by given worksheet name and cell reference, so the -// content before the page break will be printed on one page and after the -// page break on another. +// ends and where begins the next one by given worksheet name and cell +// reference, so the content before the page break will be printed on one page +// and after the page break on another. func (f *File) InsertPageBreak(sheet, cell string) error { - var ( - ws *xlsxWorksheet - row, col int - err error - ) - rowBrk, colBrk := -1, -1 - if ws, err = f.workSheetReader(sheet); err != nil { + ws, err := f.workSheetReader(sheet) + if err != nil { return err } + return ws.insertPageBreak(cell) +} + +// insertPageBreak create a page break in the worksheet by specific cell +// reference. +func (ws *xlsxWorksheet) insertPageBreak(cell string) error { + var ( + row, col int + err error + rowBrk, colBrk = -1, -1 + ) if col, row, err = CellNameToCoordinates(cell); err != nil { return err } @@ -1638,10 +1644,10 @@ func (f *File) InsertPageBreak(sheet, cell string) error { return err } if ws.RowBreaks == nil { - ws.RowBreaks = &xlsxBreaks{} + ws.RowBreaks = &xlsxRowBreaks{} } if ws.ColBreaks == nil { - ws.ColBreaks = &xlsxBreaks{} + ws.ColBreaks = &xlsxColBreaks{} } for idx, brk := range ws.RowBreaks.Brk { diff --git a/stream.go b/stream.go index d348ebaad5..02844b33bd 100644 --- a/stream.go +++ b/stream.go @@ -25,7 +25,7 @@ import ( // StreamWriter defined the type of stream writer. type StreamWriter struct { - File *File + file *File Sheet string SheetID int sheetWritten bool @@ -107,7 +107,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { return nil, newNoExistSheetError(sheet) } sw := &StreamWriter{ - File: f, + file: f, Sheet: sheet, SheetID: sheetID, } @@ -169,7 +169,7 @@ func (sw *StreamWriter) AddTable(hCell, vCell, opts string) error { } // Correct table reference range, such correct C1:B3 to B1:C3. - ref, err := sw.File.coordinatesToRangeRef(coordinates) + ref, err := sw.file.coordinatesToRangeRef(coordinates) if err != nil { return err } @@ -187,7 +187,7 @@ func (sw *StreamWriter) AddTable(hCell, vCell, opts string) error { } } - tableID := sw.File.countTables() + 1 + tableID := sw.file.countTables() + 1 name := options.TableName if name == "" { @@ -220,17 +220,17 @@ func (sw *StreamWriter) AddTable(hCell, vCell, opts string) error { tableXML := strings.ReplaceAll(sheetRelationshipsTableXML, "..", "xl") // Add first table for given sheet. - sheetPath := sw.File.sheetMap[trimSheetName(sw.Sheet)] + sheetPath := sw.file.sheetMap[trimSheetName(sw.Sheet)] sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" - rID := sw.File.addRels(sheetRels, SourceRelationshipTable, sheetRelationshipsTableXML, "") + rID := sw.file.addRels(sheetRels, SourceRelationshipTable, sheetRelationshipsTableXML, "") sw.tableParts = fmt.Sprintf(``, rID) - if err = sw.File.addContentTypePart(tableID, "table"); err != nil { + if err = sw.file.addContentTypePart(tableID, "table"); err != nil { return err } b, _ := xml.Marshal(table) - sw.File.saveFileList(tableXML, b) + sw.file.saveFileList(tableXML, b) return err } @@ -243,7 +243,7 @@ func (sw *StreamWriter) getRowValues(hRow, hCol, vCol int) (res []string, err er return nil, err } - dec := sw.File.xmlNewDecoder(r) + dec := sw.file.xmlNewDecoder(r) for { token, err := dec.Token() if err == io.EOF { @@ -269,7 +269,7 @@ func (sw *StreamWriter) getRowValues(hRow, hCol, vCol int) (res []string, err er if col < hCol || col > vCol { continue } - res[col-hCol], _ = c.getValueFrom(sw.File, nil, false) + res[col-hCol], _ = c.getValueFrom(sw.file, nil, false) } return res, nil } @@ -438,6 +438,14 @@ func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { return nil } +// InsertPageBreak create a page break to determine where the printed page +// ends and where begins the next one by given worksheet name and cell +// reference, so the content before the page break will be printed on one page +// and after the page break on another. +func (sw *StreamWriter) InsertPageBreak(cell string) error { + return sw.worksheet.insertPageBreak(cell) +} + // SetPanes provides a function to create and remove freeze panes and split // panes by given worksheet name and panes options for the StreamWriter. Note // that you must call the 'SetPanes' function before the 'SetRow' function. @@ -475,7 +483,7 @@ func setCellFormula(c *xlsxC, formula string) { // setCellTime provides a function to set number of a cell with a time. func (sw *StreamWriter) setCellTime(c *xlsxC, val time.Time) error { var date1904, isNum bool - wb, err := sw.File.workbookReader() + wb, err := sw.file.workbookReader() if err != nil { return err } @@ -483,7 +491,7 @@ func (sw *StreamWriter) setCellTime(c *xlsxC, val time.Time) error { date1904 = wb.WorkbookPr.Date1904 } if isNum, err = c.setCellTime(val, date1904); err == nil && isNum && c.S == 0 { - style, _ := sw.File.NewStyle(&Style{NumFmt: 22}) + style, _ := sw.file.NewStyle(&Style{NumFmt: 22}) c.S = style } return nil @@ -643,10 +651,10 @@ func (sw *StreamWriter) Flush() error { return err } - sheetPath := sw.File.sheetMap[trimSheetName(sw.Sheet)] - sw.File.Sheet.Delete(sheetPath) - delete(sw.File.checked, sheetPath) - sw.File.Pkg.Delete(sheetPath) + sheetPath := sw.file.sheetMap[trimSheetName(sw.Sheet)] + sw.file.Sheet.Delete(sheetPath) + delete(sw.file.checked, sheetPath) + sw.file.Pkg.Delete(sheetPath) return nil } diff --git a/stream_test.go b/stream_test.go index 925a6a789a..bdf634ea3d 100644 --- a/stream_test.go +++ b/stream_test.go @@ -234,6 +234,19 @@ func TestStreamMergeCells(t *testing.T) { assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamMergeCells.xlsx"))) } +func TestStreamInsertPageBreak(t *testing.T) { + file := NewFile() + defer func() { + assert.NoError(t, file.Close()) + }() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + assert.NoError(t, streamWriter.InsertPageBreak("A1")) + assert.NoError(t, streamWriter.Flush()) + // Save spreadsheet by the given path. + assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamInsertPageBreak.xlsx"))) +} + func TestNewStreamWriter(t *testing.T) { // Test error exceptions file := NewFile() diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 24f5e4e5f9..263c2a30ca 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -44,8 +44,8 @@ type xlsxWorksheet struct { PageMargins *xlsxPageMargins `xml:"pageMargins"` PageSetUp *xlsxPageSetUp `xml:"pageSetup"` HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` - RowBreaks *xlsxBreaks `xml:"rowBreaks"` - ColBreaks *xlsxBreaks `xml:"colBreaks"` + RowBreaks *xlsxRowBreaks `xml:"rowBreaks"` + ColBreaks *xlsxColBreaks `xml:"colBreaks"` CustomProperties *xlsxInnerXML `xml:"customProperties"` CellWatches *xlsxInnerXML `xml:"cellWatches"` IgnoredErrors *xlsxInnerXML `xml:"ignoredErrors"` @@ -358,6 +358,18 @@ type xlsxBrk struct { Pt bool `xml:"pt,attr,omitempty"` } +// xlsxRowBreaks directly maps a collection of the row breaks. +type xlsxRowBreaks struct { + XMLName xml.Name `xml:"rowBreaks"` + xlsxBreaks +} + +// xlsxRowBreaks directly maps a collection of the column breaks. +type xlsxColBreaks struct { + XMLName xml.Name `xml:"colBreaks"` + xlsxBreaks +} + // xlsxBreaks directly maps a collection of the row or column breaks. type xlsxBreaks struct { Brk []*xlsxBrk `xml:"brk"` From dde6b9c00135cefffdd9c64b7f22cfdc34c28e47 Mon Sep 17 00:00:00 2001 From: devloppper <76152313+devloppper@users.noreply.github.com> Date: Tue, 22 Nov 2022 00:14:03 +0800 Subject: [PATCH 133/213] This closes #1396, fix formula fn ADDRESS result error with empty worksheet name (#1397) - Update unit tests Co-authored-by: jayhoo --- calc.go | 7 ++----- calc_test.go | 11 ++++++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/calc.go b/calc.go index b4090c9de4..9c360c19bf 100644 --- a/calc.go +++ b/calc.go @@ -13960,13 +13960,10 @@ func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg { } var sheetText string if argsList.Len() == 5 { - sheetText = trimSheetName(argsList.Back().Value.(formulaArg).Value()) - } - if len(sheetText) > 0 { - sheetText = fmt.Sprintf("%s!", sheetText) + sheetText = fmt.Sprintf("%s!", trimSheetName(argsList.Back().Value.(formulaArg).Value())) } formatter := addressFmtMaps[fmt.Sprintf("%d_%s", int(absNum.Number), a1.Value())] - addr, err := formatter(int(colNum.Number), int(colNum.Number)) + addr, err := formatter(int(colNum.Number), int(rowNum.Number)) if err != nil { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } diff --git a/calc_test.go b/calc_test.go index 5d61712f9b..1376c0066e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1800,14 +1800,23 @@ func TestCalcCellValue(t *testing.T) { // Excel Lookup and Reference Functions // ADDRESS "=ADDRESS(1,1,1,TRUE)": "$A$1", + "=ADDRESS(1,2,1,TRUE)": "$B$1", "=ADDRESS(1,1,1,FALSE)": "R1C1", + "=ADDRESS(1,2,1,FALSE)": "R1C2", "=ADDRESS(1,1,2,TRUE)": "A$1", + "=ADDRESS(1,2,2,TRUE)": "B$1", "=ADDRESS(1,1,2,FALSE)": "R1C[1]", + "=ADDRESS(1,2,2,FALSE)": "R1C[2]", "=ADDRESS(1,1,3,TRUE)": "$A1", + "=ADDRESS(1,2,3,TRUE)": "$B1", "=ADDRESS(1,1,3,FALSE)": "R[1]C1", + "=ADDRESS(1,2,3,FALSE)": "R[1]C2", "=ADDRESS(1,1,4,TRUE)": "A1", + "=ADDRESS(1,2,4,TRUE)": "B1", "=ADDRESS(1,1,4,FALSE)": "R[1]C[1]", - "=ADDRESS(1,1,4,TRUE,\"\")": "A1", + "=ADDRESS(1,2,4,FALSE)": "R[1]C[2]", + "=ADDRESS(1,1,4,TRUE,\"\")": "!A1", + "=ADDRESS(1,2,4,TRUE,\"\")": "!B1", "=ADDRESS(1,1,4,TRUE,\"Sheet1\")": "Sheet1!A1", // CHOOSE "=CHOOSE(4,\"red\",\"blue\",\"green\",\"brown\")": "brown", From c0713951c8d95fba3a23da39bfb5c85d858d2338 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 29 Nov 2022 00:03:49 +0800 Subject: [PATCH 134/213] This closes #1404, fixes the insert picture problem in some cases - Updates unit tests - Updates documentation for stream mode functions - Updates hyperlinks in the documentation --- chart_test.go | 5 +---- picture.go | 12 ++++++++---- sheet.go | 2 +- stream.go | 18 +++++++++--------- xmlWorkbook.go | 2 +- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/chart_test.go b/chart_test.go index efce55dcd0..61e7c150f3 100644 --- a/chart_test.go +++ b/chart_test.go @@ -165,7 +165,7 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.AddChart("Sheet2", "P64", `{"type":"barPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked 100% Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet2", "X64", `{"type":"bar3DClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Clustered Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet2", "P80", `{"type":"bar3DStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Stacked Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","y_axis":{"maximum":7.5,"minimum":0.5}}`)) - assert.NoError(t, f.AddChart("Sheet2", "X80", `{"type":"bar3DPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"reverse_order":true,"maximum":0,"minimum":0},"y_axis":{"reverse_order":true,"maximum":0,"minimum":0}}`)) + assert.NoError(t, f.AddChart("Sheet2", "X80", `{"type":"bar3DPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"reverse_order":true,"minimum":0},"y_axis":{"reverse_order":true,"maximum":0,"minimum":0}}`)) // area series charts assert.NoError(t, f.AddChart("Sheet2", "AF1", `{"type":"area","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet2", "AN1", `{"type":"areaStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) @@ -264,9 +264,6 @@ func TestAddChartSheet(t *testing.T) { assert.NoError(t, f.UpdateLinkedValue()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChartSheet.xlsx"))) - // Test add chart sheet with unsupported charset drawing XML. - f.Pkg.Store("xl/drawings/drawing4.xml", MacintoshCyrillicCharset) - assert.EqualError(t, f.AddChartSheet("Chart3", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"title":{"name":"2D Column Chart"}}`), "XML syntax error on line 1: invalid UTF-8") // Test add chart sheet with unsupported charset content types. f = NewFile() f.ContentTypes = nil diff --git a/picture.go b/picture.go index 4e8a652a25..b4629b0dd7 100644 --- a/picture.go +++ b/picture.go @@ -250,20 +250,24 @@ func (f *File) addSheetPicture(sheet string, rID int) error { // countDrawings provides a function to get drawing files count storage in the // folder xl/drawings. -func (f *File) countDrawings() (count int) { +func (f *File) countDrawings() int { + var c1, c2 int f.Pkg.Range(func(k, v interface{}) bool { if strings.Contains(k.(string), "xl/drawings/drawing") { - count++ + c1++ } return true }) f.Drawings.Range(func(rel, value interface{}) bool { if strings.Contains(rel.(string), "xl/drawings/drawing") { - count++ + c2++ } return true }) - return + if c1 < c2 { + return c2 + } + return c1 } // addDrawingPicture provides a function to add picture by given sheet, diff --git a/sheet.go b/sheet.go index 3ac933bc21..e241abd8b4 100644 --- a/sheet.go +++ b/sheet.go @@ -656,7 +656,7 @@ func (f *File) copySheet(from, to int) error { // SetSheetVisible provides a function to set worksheet visible by given worksheet // name. A workbook must contain at least one visible worksheet. If the given // worksheet has been activated, this setting will be invalidated. Sheet state -// values as defined by https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetstatevalues +// values as defined by https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetstatevalues // // visible // hidden diff --git a/stream.go b/stream.go index 02844b33bd..09baa4206e 100644 --- a/stream.go +++ b/stream.go @@ -354,9 +354,9 @@ func parseRowOpts(opts ...RowOpts) *RowOpts { return options } -// SetRow writes an array to stream rows by giving a worksheet name, starting -// coordinate and a pointer to an array of values. Note that you must call the -// 'Flush' method to end the streaming writing process. +// SetRow writes an array to stream rows by giving starting cell reference and a +// pointer to an array of values. Note that you must call the 'Flush' function +// to end the streaming writing process. // // As a special case, if Cell is used as a value, then the Cell.StyleID will be // applied to that cell. @@ -438,17 +438,17 @@ func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { return nil } -// InsertPageBreak create a page break to determine where the printed page -// ends and where begins the next one by given worksheet name and cell -// reference, so the content before the page break will be printed on one page -// and after the page break on another. +// InsertPageBreak creates a page break to determine where the printed page ends +// and where begins the next one by a given cell reference, the content before +// the page break will be printed on one page and after the page break on +// another. func (sw *StreamWriter) InsertPageBreak(cell string) error { return sw.worksheet.insertPageBreak(cell) } // SetPanes provides a function to create and remove freeze panes and split -// panes by given worksheet name and panes options for the StreamWriter. Note -// that you must call the 'SetPanes' function before the 'SetRow' function. +// panes by giving panes options for the StreamWriter. Note that you must call +// the 'SetPanes' function before the 'SetRow' function. func (sw *StreamWriter) SetPanes(panes string) error { if sw.sheetWritten { return ErrStreamSetPanes diff --git a/xmlWorkbook.go b/xmlWorkbook.go index dcfa6cad15..e384807c7a 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -219,7 +219,7 @@ type xlsxDefinedNames struct { // http://schemas.openxmlformats.org/spreadsheetml/2006/main This element // defines a defined name within this workbook. A defined name is descriptive // text that is used to represents a cell, range of cells, formula, or constant -// value. For a descriptions of the attributes see https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.definedname +// value. For a descriptions of the attributes see https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.definedname type xlsxDefinedName struct { Comment string `xml:"comment,attr,omitempty"` CustomMenu string `xml:"customMenu,attr,omitempty"` From 5e0953d7783ce65707fa89f5a773697b69e82e96 Mon Sep 17 00:00:00 2001 From: jianxinhou <51222175+jianxinhou@users.noreply.github.com> Date: Thu, 1 Dec 2022 10:44:28 +0800 Subject: [PATCH 135/213] This closes #1405, add new function SetSheetBackgroundFromBytes (#1406) Co-authored-by: houjianxin.rupert --- picture.go | 7 +++++-- sheet.go | 27 ++++++++++++++++++++++----- sheet_test.go | 24 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/picture.go b/picture.go index b4629b0dd7..045e2af057 100644 --- a/picture.go +++ b/picture.go @@ -38,7 +38,8 @@ func parsePictureOptions(opts string) (*pictureOptions, error) { // AddPicture provides the method to add picture in a sheet by given picture // format set (such as offset, scale, aspect ratio setting and print settings) -// and file path. This function is concurrency safe. For example: +// and file path, supported image types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, +// TIF, TIFF, WMF, and WMZ. This function is concurrency safe. For example: // // package main // @@ -121,7 +122,9 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // AddPictureFromBytes provides the method to add picture in a sheet by given // picture format set (such as offset, scale, aspect ratio setting and print -// settings), file base name, extension name and file bytes. For example: +// settings), file base name, extension name and file bytes, supported image +// types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. For +// example: // // package main // diff --git a/sheet.go b/sheet.go index e241abd8b4..bbf529ae82 100644 --- a/sheet.go +++ b/sheet.go @@ -485,23 +485,40 @@ func (f *File) getSheetXMLPath(sheet string) (string, bool) { } // SetSheetBackground provides a function to set background picture by given -// worksheet name and file path. +// worksheet name and file path. Supported image types: EMF, EMZ, GIF, JPEG, +// JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. func (f *File) SetSheetBackground(sheet, picture string) error { var err error // Check picture exists first. if _, err = os.Stat(picture); os.IsNotExist(err) { return err } - ext, ok := supportedImageTypes[path.Ext(picture)] + file, _ := os.ReadFile(filepath.Clean(picture)) + return f.setSheetBackground(sheet, path.Ext(picture), file) +} + +// SetSheetBackgroundFromBytes provides a function to set background picture by +// given worksheet name, extension name and image data. Supported image types: +// EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. +func (f *File) SetSheetBackgroundFromBytes(sheet, extension string, picture []byte) error { + if len(picture) == 0 { + return ErrParameterInvalid + } + return f.setSheetBackground(sheet, extension, picture) +} + +// setSheetBackground provides a function to set background picture by given +// worksheet name, file name extension and image data. +func (f *File) setSheetBackground(sheet, extension string, file []byte) error { + imageType, ok := supportedImageTypes[extension] if !ok { return ErrImgExt } - file, _ := os.ReadFile(filepath.Clean(picture)) - name := f.addMedia(file, ext) + name := f.addMedia(file, imageType) sheetXMLPath, _ := f.getSheetXMLPath(sheet) sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") - if err = f.addSheetPicture(sheet, rID); err != nil { + if err := f.addSheetPicture(sheet, rID); err != nil { return err } f.addSheetNameSpace(sheet, SourceRelationship) diff --git a/sheet_test.go b/sheet_test.go index 08c7c1a9ea..2494cfba1c 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -3,6 +3,8 @@ package excelize import ( "encoding/xml" "fmt" + "io" + "os" "path/filepath" "strconv" "strings" @@ -463,3 +465,25 @@ func TestAttrValToFloat(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 42.1, got) } + +func TestSetSheetBackgroundFromBytes(t *testing.T) { + f := NewFile() + f.SetSheetName("Sheet1", ".svg") + for i, imageTypes := range []string{".svg", ".emf", ".emz", ".gif", ".jpg", ".png", ".tif", ".wmf", ".wmz"} { + file := fmt.Sprintf("excelize%s", imageTypes) + if i > 0 { + file = filepath.Join("test", "images", fmt.Sprintf("excel%s", imageTypes)) + f.NewSheet(imageTypes) + } + img, err := os.Open(file) + assert.NoError(t, err) + content, err := io.ReadAll(img) + assert.NoError(t, err) + assert.NoError(t, img.Close()) + assert.NoError(t, f.SetSheetBackgroundFromBytes(imageTypes, imageTypes, content)) + } + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetBackgroundFromBytes.xlsx"))) + assert.NoError(t, f.Close()) + + assert.EqualError(t, f.SetSheetBackgroundFromBytes("Sheet1", ".svg", nil), ErrParameterInvalid.Error()) +} From 61fda0b1cad43ef7ce88ab7ad36be69fcd8cf8b3 Mon Sep 17 00:00:00 2001 From: nesstord <56038047+nesstord@users.noreply.github.com> Date: Tue, 6 Dec 2022 23:45:27 +0700 Subject: [PATCH 136/213] Fix binary string regex (#1415) --- lib.go | 19 ++++--------------- lib_test.go | 7 ++++--- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/lib.go b/lib.go index 16170a7183..27b5ab772b 100644 --- a/lib.go +++ b/lib.go @@ -702,8 +702,8 @@ func isNumeric(s string) (bool, int, float64) { } var ( - bstrExp = regexp.MustCompile(`_x[a-zA-Z\d]{4}_`) - bstrEscapeExp = regexp.MustCompile(`x[a-zA-Z\d]{4}_`) + bstrExp = regexp.MustCompile(`_x[a-fA-F\d]{4}_`) + bstrEscapeExp = regexp.MustCompile(`x[a-fA-F\d]{4}_`) ) // bstrUnmarshal parses the binary basic string, this will trim escaped string @@ -729,16 +729,7 @@ func bstrUnmarshal(s string) (result string) { } if bstrExp.MatchString(subStr) { cursor = match[1] - v, err := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`) - if err != nil { - if l > match[1]+6 && bstrEscapeExp.MatchString(s[match[1]:match[1]+6]) { - result += subStr[:6] - cursor = match[1] + 6 - continue - } - result += subStr - continue - } + v, _ := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`) result += v } } @@ -769,12 +760,10 @@ func bstrMarshal(s string) (result string) { } if bstrExp.MatchString(subStr) { cursor = match[1] - _, err := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`) - if err == nil { + if _, err := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`); err == nil { result += "_x005F" + subStr continue } - result += subStr } } if cursor < l { diff --git a/lib_test.go b/lib_test.go index e96704f67c..ec5fd640db 100644 --- a/lib_test.go +++ b/lib_test.go @@ -305,18 +305,19 @@ func TestBstrUnmarshal(t *testing.T) { "*_x0008_*": "*\b*", "*_x4F60__x597D_": "*你好", "*_xG000_": "*_xG000_", - "*_xG05F_x0001_*": "*_xG05F*", + "*_xG05F_x0001_*": "*_xG05F\x01*", "*_x005F__x0008_*": "*_\b*", "*_x005F_x0001_*": "*_x0001_*", "*_x005f_x005F__x0008_*": "*_x005F_\b*", - "*_x005F_x005F_xG05F_x0006_*": "*_x005F_xG05F*", + "*_x005F_x005F_xG05F_x0006_*": "*_x005F_xG05F\x06*", "*_x005F_x005F_x005F_x0006_*": "*_x005F_x0006_*", "_x005F__x0008_******": "_\b******", "******_x005F__x0008_": "******_\b", "******_x005F__x0008_******": "******_\b******", + "_x000x_x005F_x000x_": "_x000x_x000x_", } for bstr, expected := range bstrs { - assert.Equal(t, expected, bstrUnmarshal(bstr)) + assert.Equal(t, expected, bstrUnmarshal(bstr), bstr) } } From ce4f7a25c98b9e8fc13b32f0f88b4056fd8da7d7 Mon Sep 17 00:00:00 2001 From: Bayzet Tlyupov Date: Mon, 19 Dec 2022 04:28:43 +0300 Subject: [PATCH 137/213] This closes #1416, support set row outline level to stream (#1422) Co-authored-by: TlyupovBM --- stream.go | 16 +++++++++++++--- stream_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/stream.go b/stream.go index 09baa4206e..a575e761dd 100644 --- a/stream.go +++ b/stream.go @@ -310,9 +310,10 @@ type Cell struct { // RowOpts define the options for the set row, it can be used directly in // StreamWriter.SetRow to specify the style and properties of the row. type RowOpts struct { - Height float64 - Hidden bool - StyleID int + Height float64 + Hidden bool + StyleID int + OutlineLevel int } // marshalAttrs prepare attributes of the row. @@ -328,6 +329,10 @@ func (r *RowOpts) marshalAttrs() (strings.Builder, error) { err = ErrMaxRowHeight return attrs, err } + if r.OutlineLevel > 7 { + err = ErrOutlineLevel + return attrs, err + } if r.StyleID > 0 { attrs.WriteString(` s="`) attrs.WriteString(strconv.Itoa(r.StyleID)) @@ -338,6 +343,11 @@ func (r *RowOpts) marshalAttrs() (strings.Builder, error) { attrs.WriteString(strconv.FormatFloat(r.Height, 'f', -1, 64)) attrs.WriteString(`" customHeight="1"`) } + if r.OutlineLevel > 0 { + attrs.WriteString(` outlineLevel="`) + attrs.WriteString(strconv.Itoa(r.OutlineLevel)) + attrs.WriteString(`"`) + } if r.Hidden { attrs.WriteString(` hidden="1"`) } diff --git a/stream_test.go b/stream_test.go index bdf634ea3d..41f54151c7 100644 --- a/stream_test.go +++ b/stream_test.go @@ -358,3 +358,31 @@ func TestStreamSetCellValFunc(t *testing.T) { assert.NoError(t, sw.setCellValFunc(c, nil)) assert.NoError(t, sw.setCellValFunc(c, complex64(5+10i))) } + +func TestStreamWriterOutlineLevel(t *testing.T) { + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + + // Test set outlineLevel in row. + assert.NoError(t, streamWriter.SetRow("A1", nil, RowOpts{OutlineLevel: 1})) + assert.NoError(t, streamWriter.SetRow("A2", nil, RowOpts{OutlineLevel: 7})) + assert.ErrorIs(t, ErrOutlineLevel, streamWriter.SetRow("A3", nil, RowOpts{OutlineLevel: 8})) + + assert.NoError(t, streamWriter.Flush()) + // Save spreadsheet by the given path. + assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriterSetRowOutlineLevel.xlsx"))) + + file, err = OpenFile(filepath.Join("test", "TestStreamWriterSetRowOutlineLevel.xlsx")) + assert.NoError(t, err) + level, err := file.GetRowOutlineLevel("Sheet1", 1) + assert.NoError(t, err) + assert.Equal(t, uint8(1), level) + level, err = file.GetRowOutlineLevel("Sheet1", 2) + assert.NoError(t, err) + assert.Equal(t, uint8(7), level) + level, err = file.GetRowOutlineLevel("Sheet1", 3) + assert.NoError(t, err) + assert.Equal(t, uint8(0), level) + assert.NoError(t, file.Close()) +} From 6a5ee811ba200ce9524720822507477131401f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E4=BC=9F=E5=8C=A1?= <673411814@qq.com> Date: Fri, 23 Dec 2022 00:54:40 +0800 Subject: [PATCH 138/213] This closes #1425, breaking changes for sheet name (#1426) - Checking and return error for invalid sheet name instead of trim invalid characters - Add error return for the 4 functions: `DeleteSheet`, `GetSheetIndex`, `GetSheetVisible` and `SetSheetName` - Export new error 4 constants: `ErrSheetNameBlank`, `ErrSheetNameInvalid`, `ErrSheetNameLength` and `ErrSheetNameSingleQuote` - Rename exported error constant `ErrExistsWorksheet` to `ErrExistsSheet` - Update unit tests for 90 functions: `AddChart`, `AddChartSheet`, `AddComment`, `AddDataValidation`, `AddPicture`, `AddPictureFromBytes`, `AddPivotTable`, `AddShape`, `AddSparkline`, `AddTable`, `AutoFilter`, `CalcCellValue`, `Cols`, `DeleteChart`, `DeleteComment`, `DeleteDataValidation`, `DeletePicture`, `DeleteSheet`, `DuplicateRow`, `DuplicateRowTo`, `GetCellFormula`, `GetCellHyperLink`, `GetCellRichText`, `GetCellStyle`, `GetCellType`, `GetCellValue`, `GetColOutlineLevel`, `GetCols`, `GetColStyle`, `GetColVisible`, `GetColWidth`, `GetConditionalFormats`, `GetDataValidations`, `GetMergeCells`, `GetPageLayout`, `GetPageMargins`, `GetPicture`, `GetRowHeight`, `GetRowOutlineLevel`, `GetRows`, `GetRowVisible`, `GetSheetIndex`, `GetSheetProps`, `GetSheetVisible`, `GroupSheets`, `InsertCol`, `InsertPageBreak`, `InsertRows`, `MergeCell`, `NewSheet`, `NewStreamWriter`, `ProtectSheet`, `RemoveCol`, `RemovePageBreak`, `RemoveRow`, `Rows`, `SearchSheet`, `SetCellBool`, `SetCellDefault`, `SetCellFloat`, `SetCellFormula`, `SetCellHyperLink`, `SetCellInt`, `SetCellRichText`, `SetCellStr`, `SetCellStyle`, `SetCellValue`, `SetColOutlineLevel`, `SetColStyle`, `SetColVisible`, `SetColWidth`, `SetConditionalFormat`, `SetHeaderFooter`, `SetPageLayout`, `SetPageMargins`, `SetPanes`, `SetRowHeight`, `SetRowOutlineLevel`, `SetRowStyle`, `SetRowVisible`, `SetSheetBackground`, `SetSheetBackgroundFromBytes`, `SetSheetCol`, `SetSheetName`, `SetSheetProps`, `SetSheetRow`, `SetSheetVisible`, `UnmergeCell`, `UnprotectSheet` and `UnsetConditionalFormat` - Update documentation of the set style functions Co-authored-by: guoweikuang --- calc.go | 11 +-- calc_test.go | 9 ++- cell_test.go | 92 +++++++++++++-------- chart.go | 10 ++- chart_test.go | 16 ++-- col.go | 5 +- col_test.go | 81 +++++++++++++------ comment.go | 3 + comment_test.go | 17 ++-- datavalidation_test.go | 18 +++-- errors.go | 17 +++- excelize.go | 3 + excelize_test.go | 176 +++++++++++++++++++++++------------------ merge_test.go | 21 +++-- picture_test.go | 63 +++++++++------ pivotTable_test.go | 6 ++ rows.go | 5 +- rows_test.go | 75 ++++++++++++------ shape_test.go | 8 +- sheet.go | 170 +++++++++++++++++++++------------------ sheet_test.go | 124 +++++++++++++++++++++++------ sheetpr_test.go | 18 ++++- sparkline_test.go | 10 ++- stream.go | 9 ++- stream_test.go | 3 + styles.go | 42 +++++++--- styles_test.go | 29 ++++--- table.go | 5 +- table_test.go | 49 ++++-------- workbook.go | 2 +- xmlComments.go | 2 +- xmlDrawing.go | 117 +++++++++++++-------------- 32 files changed, 761 insertions(+), 455 deletions(-) diff --git a/calc.go b/calc.go index 9c360c19bf..9ab5eeb2fe 100644 --- a/calc.go +++ b/calc.go @@ -11460,19 +11460,20 @@ func (fn *formulaFuncs) SHEET(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "SHEET accepts at most 1 argument") } if argsList.Len() == 0 { - return newNumberFormulaArg(float64(fn.f.GetSheetIndex(fn.sheet) + 1)) + idx, _ := fn.f.GetSheetIndex(fn.sheet) + return newNumberFormulaArg(float64(idx + 1)) } arg := argsList.Front().Value.(formulaArg) - if sheetIdx := fn.f.GetSheetIndex(arg.Value()); sheetIdx != -1 { + if sheetIdx, _ := fn.f.GetSheetIndex(arg.Value()); sheetIdx != -1 { return newNumberFormulaArg(float64(sheetIdx + 1)) } if arg.cellRanges != nil && arg.cellRanges.Len() > 0 { - if sheetIdx := fn.f.GetSheetIndex(arg.cellRanges.Front().Value.(cellRange).From.Sheet); sheetIdx != -1 { + if sheetIdx, _ := fn.f.GetSheetIndex(arg.cellRanges.Front().Value.(cellRange).From.Sheet); sheetIdx != -1 { return newNumberFormulaArg(float64(sheetIdx + 1)) } } if arg.cellRefs != nil && arg.cellRefs.Len() > 0 { - if sheetIdx := fn.f.GetSheetIndex(arg.cellRefs.Front().Value.(cellRef).Sheet); sheetIdx != -1 { + if sheetIdx, _ := fn.f.GetSheetIndex(arg.cellRefs.Front().Value.(cellRef).Sheet); sheetIdx != -1 { return newNumberFormulaArg(float64(sheetIdx + 1)) } } @@ -13960,7 +13961,7 @@ func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg { } var sheetText string if argsList.Len() == 5 { - sheetText = fmt.Sprintf("%s!", trimSheetName(argsList.Back().Value.(formulaArg).Value())) + sheetText = fmt.Sprintf("%s!", argsList.Back().Value.(formulaArg).Value()) } formatter := addressFmtMaps[fmt.Sprintf("%d_%s", int(absNum.Number), a1.Value())] addr, err := formatter(int(colNum.Number), int(rowNum.Number)) diff --git a/calc_test.go b/calc_test.go index 1376c0066e..1c1f4d53c3 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4389,16 +4389,19 @@ func TestCalcCellValue(t *testing.T) { assert.NoError(t, err) } - // Test get calculated cell value on not formula cell. + // Test get calculated cell value on not formula cell f := prepareCalcData(cellData) result, err := f.CalcCellValue("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, "", result) - // Test get calculated cell value on not exists worksheet. + // Test get calculated cell value on not exists worksheet f = prepareCalcData(cellData) _, err = f.CalcCellValue("SheetN", "A1") assert.EqualError(t, err, "sheet SheetN does not exist") - // Test get calculated cell value with not support formula. + // Test get calculated cell value with invalid sheet name + _, err = f.CalcCellValue("Sheet:1", "A1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + // Test get calculated cell value with not support formula f = prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=UNSUPPORT(A1)")) _, err = f.CalcCellValue("Sheet1", "A1") diff --git a/cell_test.go b/cell_test.go index 40bab9b6d1..69a3f81d32 100644 --- a/cell_test.go +++ b/cell_test.go @@ -167,13 +167,15 @@ func TestSetCellFloat(t *testing.T) { }) f := NewFile() assert.EqualError(t, f.SetCellFloat(sheet, "A", 123.42, -1, 64), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + // Test set cell float data type value with invalid sheet name + assert.EqualError(t, f.SetCellFloat("Sheet:1", "A1", 123.42, -1, 64), ErrSheetNameInvalid.Error()) } func TestSetCellValue(t *testing.T) { f := NewFile() assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Duration(1e13)), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - // Test set cell value with column and row style inherit. + // Test set cell value with column and row style inherit style1, err := f.NewStyle(&Style{NumFmt: 2}) assert.NoError(t, err) style2, err := f.NewStyle(&Style{NumFmt: 9}) @@ -189,11 +191,13 @@ func TestSetCellValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "0.50", B2) - // Test set cell value with unsupported charset shared strings table. + // Test set cell value with invalid sheet name + assert.EqualError(t, f.SetCellValue("Sheet:1", "A1", "A1"), ErrSheetNameInvalid.Error()) + // Test set cell value with unsupported charset shared strings table f.SharedStrings = nil f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) assert.EqualError(t, f.SetCellValue("Sheet1", "A1", "A1"), "XML syntax error on line 1: invalid UTF-8") - // Test set cell value with unsupported charset workbook. + // Test set cell value with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.SetCellValue("Sheet1", "A1", time.Now().UTC()), "XML syntax error on line 1: invalid UTF-8") @@ -208,7 +212,7 @@ func TestSetCellValues(t *testing.T) { assert.NoError(t, err) assert.Equal(t, v, "12/31/10 00:00") - // Test date value lower than min date supported by Excel. + // Test date value lower than min date supported by Excel err = f.SetCellValue("Sheet1", "A1", time.Date(1600, time.December, 31, 0, 0, 0, 0, time.UTC)) assert.NoError(t, err) @@ -220,6 +224,8 @@ func TestSetCellValues(t *testing.T) { func TestSetCellBool(t *testing.T) { f := NewFile() assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + // Test set cell boolean data type value with invalid sheet name + assert.EqualError(t, f.SetCellBool("Sheet:1", "A1", true), ErrSheetNameInvalid.Error()) } func TestSetCellTime(t *testing.T) { @@ -242,7 +248,7 @@ func TestSetCellTime(t *testing.T) { } func TestGetCellValue(t *testing.T) { - // Test get cell value without r attribute of the row. + // Test get cell value without r attribute of the row f := NewFile() sheetData := `%s` @@ -387,11 +393,14 @@ func TestGetCellValue(t *testing.T) { }, rows[0]) assert.NoError(t, err) - // Test get cell value with unsupported charset shared strings table. + // Test get cell value with unsupported charset shared strings table f.SharedStrings = nil f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) _, value := f.GetCellValue("Sheet1", "A1") assert.EqualError(t, value, "XML syntax error on line 1: invalid UTF-8") + // Test get cell value with invalid sheet name + _, err = f.GetCellValue("Sheet:1", "A1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) } func TestGetCellType(t *testing.T) { @@ -405,6 +414,9 @@ func TestGetCellType(t *testing.T) { assert.Equal(t, CellTypeSharedString, cellType) _, err = f.GetCellType("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + // Test get cell type with invalid sheet name + _, err = f.GetCellType("Sheet:1", "A1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) } func TestGetValueFrom(t *testing.T) { @@ -418,12 +430,16 @@ func TestGetValueFrom(t *testing.T) { } func TestGetCellFormula(t *testing.T) { - // Test get cell formula on not exist worksheet. + // Test get cell formula on not exist worksheet f := NewFile() _, err := f.GetCellFormula("SheetN", "A1") assert.EqualError(t, err, "sheet SheetN does not exist") - // Test get cell formula on no formula cell. + // Test get cell formula with invalid sheet name + _, err = f.GetCellFormula("Sheet:1", "A1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + + // Test get cell formula on no formula cell assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) _, err = f.GetCellFormula("Sheet1", "A1") assert.NoError(t, err) @@ -497,7 +513,10 @@ func TestSetCellFormula(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "B19", "SUM(Sheet2!D2,Sheet2!D11)")) assert.NoError(t, f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)")) - // Test set cell formula with illegal rows number. + // Test set cell formula with invalid sheet name + assert.EqualError(t, f.SetCellFormula("Sheet:1", "A1", "SUM(1,2)"), ErrSheetNameInvalid.Error()) + + // Test set cell formula with illegal rows number assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), newCellNameToCoordinatesError("C", newInvalidCellNameError("C")).Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) @@ -507,15 +526,15 @@ func TestSetCellFormula(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - // Test remove cell formula. + // Test remove cell formula assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula2.xlsx"))) - // Test remove all cell formula. + // Test remove all cell formula assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula3.xlsx"))) assert.NoError(t, f.Close()) - // Test set shared formula for the cells. + // Test set shared formula for the cells f = NewFile() for r := 1; r <= 5; r++ { assert.NoError(t, f.SetSheetRow("Sheet1", fmt.Sprintf("A%d", r), &[]interface{}{r, r + 1})) @@ -533,7 +552,7 @@ func TestSetCellFormula(t *testing.T) { assert.EqualError(t, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType}), ErrParameterInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula5.xlsx"))) - // Test set table formula for the cells. + // Test set table formula for the cells f = NewFile() for idx, row := range [][]interface{}{{"A", "B", "C"}, {1, 2}} { assert.NoError(t, f.SetSheetRow("Sheet1", fmt.Sprintf("A%d", idx+1), &row)) @@ -583,46 +602,49 @@ func TestGetCellRichText(t *testing.T) { runsSource[1].Font.Color = strings.ToUpper(runsSource[1].Font.Color) assert.True(t, reflect.DeepEqual(runsSource[1].Font, runs[1].Font), "should get the same font") - // Test get cell rich text when string item index overflow. + // Test get cell rich text when string item index overflow ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "2" runs, err = f.GetCellRichText("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, 0, len(runs)) - // Test get cell rich text when string item index is negative. + // Test get cell rich text when string item index is negative ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "-1" runs, err = f.GetCellRichText("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, 0, len(runs)) - // Test get cell rich text on invalid string item index. + // Test get cell rich text on invalid string item index ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "x" _, err = f.GetCellRichText("Sheet1", "A1") assert.EqualError(t, err, "strconv.Atoi: parsing \"x\": invalid syntax") - // Test set cell rich text on not exists worksheet. + // Test set cell rich text on not exists worksheet _, err = f.GetCellRichText("SheetN", "A1") assert.EqualError(t, err, "sheet SheetN does not exist") - // Test set cell rich text with illegal cell reference. + // Test set cell rich text with illegal cell reference _, err = f.GetCellRichText("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - // Test set rich text color theme without tint. + // Test set rich text color theme without tint assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTheme: &theme}}})) - // Test set rich text color tint without theme. + // Test set rich text color tint without theme assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTint: 0.5}}})) - // Test set cell rich text with unsupported charset shared strings table. + // Test set cell rich text with unsupported charset shared strings table f.SharedStrings = nil f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", runsSource), "XML syntax error on line 1: invalid UTF-8") - // Test get cell rich text with unsupported charset shared strings table. + // Test get cell rich text with unsupported charset shared strings table f.SharedStrings = nil f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) _, err = f.GetCellRichText("Sheet1", "A1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get cell rich text with invalid sheet name + _, err = f.GetCellRichText("Sheet:1", "A1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) } func TestSetCellRichText(t *testing.T) { @@ -716,12 +738,14 @@ func TestSetCellRichText(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellRichText.xlsx"))) - // Test set cell rich text on not exists worksheet. + // Test set cell rich text on not exists worksheet assert.EqualError(t, f.SetCellRichText("SheetN", "A1", richTextRun), "sheet SheetN does not exist") - // Test set cell rich text with illegal cell reference. + // Test set cell rich text with invalid sheet name + assert.EqualError(t, f.SetCellRichText("Sheet:1", "A1", richTextRun), ErrSheetNameInvalid.Error()) + // Test set cell rich text with illegal cell reference assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) richTextRun = []RichTextRun{{Text: strings.Repeat("s", TotalCellChars+1)}} - // Test set cell rich text with characters over the maximum limit. + // Test set cell rich text with characters over the maximum limit assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", richTextRun), ErrCellCharsLength.Error()) } @@ -747,7 +771,7 @@ func TestFormattedValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "03/04/2019", result) - // Test format value with no built-in number format ID. + // Test format value with no built-in number format ID numFmtID := 5 f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: &numFmtID, @@ -756,7 +780,7 @@ func TestFormattedValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "43528", result) - // Test format value with invalid number format ID. + // Test format value with invalid number format ID f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: nil, }) @@ -764,7 +788,7 @@ func TestFormattedValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "43528", result) - // Test format value with empty number format. + // Test format value with empty number format f.Styles.NumFmts = nil f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: &numFmtID, @@ -773,7 +797,7 @@ func TestFormattedValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "43528", result) - // Test format decimal value with build-in number format ID. + // Test format decimal value with build-in number format ID styleID, err := f.NewStyle(&Style{ NumFmt: 1, }) @@ -786,13 +810,13 @@ func TestFormattedValue(t *testing.T) { assert.Equal(t, "0_0", fn("0_0", "", false)) } - // Test format value with unsupported charset workbook. + // Test format value with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) _, err = f.formattedValue(1, "43528", false) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") - // Test format value with unsupported charset style sheet. + // Test format value with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) _, err = f.formattedValue(1, "43528", false) @@ -800,7 +824,7 @@ func TestFormattedValue(t *testing.T) { } func TestFormattedValueNilXfs(t *testing.T) { - // Set the CellXfs to nil and verify that the formattedValue function does not crash. + // Set the CellXfs to nil and verify that the formattedValue function does not crash f := NewFile() f.Styles.CellXfs = nil result, err := f.formattedValue(3, "43528", false) @@ -809,7 +833,7 @@ func TestFormattedValueNilXfs(t *testing.T) { } func TestFormattedValueNilNumFmts(t *testing.T) { - // Set the NumFmts value to nil and verify that the formattedValue function does not crash. + // Set the NumFmts value to nil and verify that the formattedValue function does not crash f := NewFile() f.Styles.NumFmts = nil result, err := f.formattedValue(3, "43528", false) @@ -818,7 +842,7 @@ func TestFormattedValueNilNumFmts(t *testing.T) { } func TestFormattedValueNilWorkbook(t *testing.T) { - // Set the Workbook value to nil and verify that the formattedValue function does not crash. + // Set the Workbook value to nil and verify that the formattedValue function does not crash f := NewFile() f.WorkBook = nil result, err := f.formattedValue(3, "43528", false) diff --git a/chart.go b/chart.go index ec5e468eea..d4ea8d9918 100644 --- a/chart.go +++ b/chart.go @@ -941,8 +941,12 @@ func (f *File) AddChart(sheet, cell, opts string, combo ...string) error { // a chart. func (f *File) AddChartSheet(sheet, opts string, combo ...string) error { // Check if the worksheet already exists - if f.GetSheetIndex(sheet) != -1 { - return ErrExistsWorksheet + idx, err := f.GetSheetIndex(sheet) + if err != nil { + return err + } + if idx != -1 { + return ErrExistsSheet } options, comboCharts, err := f.getChartOptions(opts, combo) if err != nil { @@ -963,7 +967,7 @@ func (f *File) AddChartSheet(sheet, opts string, combo ...string) error { } sheetID++ path := "xl/chartsheets/sheet" + strconv.Itoa(sheetID) + ".xml" - f.sheetMap[trimSheetName(sheet)] = path + f.sheetMap[sheet] = path f.Sheet.Store(path, nil) drawingID := f.countDrawings() + 1 chartID := f.countCharts() + 1 diff --git a/chart_test.go b/chart_test.go index 61e7c150f3..a61f1d7918 100644 --- a/chart_test.go +++ b/chart_test.go @@ -217,6 +217,8 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"areaStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[1]), fmt.Sprintf(`{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[0]))) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) + // Test with invalid sheet name + assert.EqualError(t, f.AddChart("Sheet:1", "A1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}]}`), ErrSheetNameInvalid.Error()) // Test with illegal cell reference assert.EqualError(t, f.AddChart("Sheet2", "A", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // Test with unsupported chart type @@ -257,14 +259,16 @@ func TestAddChartSheet(t *testing.T) { // Test cell value on chartsheet assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is not a worksheet") // Test add chartsheet on already existing name sheet - assert.EqualError(t, f.AddChartSheet("Sheet1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`), ErrExistsWorksheet.Error()) + assert.EqualError(t, f.AddChartSheet("Sheet1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`), ErrExistsSheet.Error()) + // Test add chartsheet with invalid sheet name + assert.EqualError(t, f.AddChartSheet("Sheet:1", "A1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`), ErrSheetNameInvalid.Error()) // Test with unsupported chart type assert.EqualError(t, f.AddChartSheet("Chart2", `{"type":"unknown","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`), "unsupported chart type unknown") assert.NoError(t, f.UpdateLinkedValue()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChartSheet.xlsx"))) - // Test add chart sheet with unsupported charset content types. + // Test add chart sheet with unsupported charset content types f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) @@ -278,11 +282,13 @@ func TestDeleteChart(t *testing.T) { assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) assert.NoError(t, f.DeleteChart("Sheet1", "P1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteChart.xlsx"))) - // Test delete chart on not exists worksheet. + // Test delete chart with invalid sheet name + assert.EqualError(t, f.DeleteChart("Sheet:1", "P1"), ErrSheetNameInvalid.Error()) + // Test delete chart on not exists worksheet assert.EqualError(t, f.DeleteChart("SheetN", "A1"), "sheet SheetN does not exist") - // Test delete chart with invalid coordinates. + // Test delete chart with invalid coordinates assert.EqualError(t, f.DeleteChart("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) - // Test delete chart on no chart worksheet. + // Test delete chart on no chart worksheet assert.NoError(t, NewFile().DeleteChart("Sheet1", "A1")) assert.NoError(t, f.Close()) } diff --git a/col.go b/col.go index b93087738c..f8906109b6 100644 --- a/col.go +++ b/col.go @@ -208,6 +208,9 @@ func (cols *Cols) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta // fmt.Println() // } func (f *File) Cols(sheet string) (*Cols, error) { + if err := checkSheetName(sheet); err != nil { + return nil, err + } name, ok := f.getSheetXMLPath(sheet) if !ok { return nil, ErrSheetNotExist{sheet} @@ -236,7 +239,7 @@ func (f *File) Cols(sheet string) (*Cols, error) { case xml.EndElement: if xmlElement.Name.Local == "sheetData" { colIterator.cols.f = f - colIterator.cols.sheet = trimSheetName(sheet) + colIterator.cols.sheet = sheet return &colIterator.cols, nil } } diff --git a/col_test.go b/col_test.go index 0ed1906166..4c5961f6e6 100644 --- a/col_test.go +++ b/col_test.go @@ -57,7 +57,13 @@ func TestCols(t *testing.T) { _, err = f.Rows("Sheet1") assert.NoError(t, err) - // Test columns iterator with unsupported charset shared strings table. + // Test columns iterator with invalid sheet name + _, err = f.Cols("Sheet:1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + // Test get columns cells with invalid sheet name + _, err = f.GetCols("Sheet:1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + // Test columns iterator with unsupported charset shared strings table f.SharedStrings = nil f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) cols, err = f.Cols("Sheet1") @@ -212,14 +218,18 @@ func TestColumnVisibility(t *testing.T) { assert.Equal(t, true, visible) assert.NoError(t, err) - // Test get column visible on an inexistent worksheet. + // Test get column visible on an inexistent worksheet _, err = f.GetColVisible("SheetN", "F") assert.EqualError(t, err, "sheet SheetN does not exist") - - // Test get column visible with illegal cell reference. + // Test get column visible with invalid sheet name + _, err = f.GetColVisible("Sheet:1", "F") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + // Test get column visible with illegal cell reference _, err = f.GetColVisible("Sheet1", "*") assert.EqualError(t, err, newInvalidColumnNameError("*").Error()) assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), newInvalidColumnNameError("*").Error()) + // Test set column visible with invalid sheet name + assert.EqualError(t, f.SetColVisible("Sheet:1", "A", false), ErrSheetNameInvalid.Error()) f.NewSheet("Sheet3") assert.NoError(t, f.SetColVisible("Sheet3", "E", false)) @@ -254,25 +264,35 @@ func TestOutlineLevel(t *testing.T) { assert.Equal(t, uint8(0), level) assert.EqualError(t, err, "sheet SheetN does not exist") + // Test column outline level with invalid sheet name + _, err = f.GetColOutlineLevel("Sheet:1", "A") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + assert.NoError(t, f.SetColWidth("Sheet2", "A", "D", 13)) assert.EqualError(t, f.SetColWidth("Sheet2", "A", "D", MaxColumnWidth+1), ErrColumnWidth.Error()) + // Test set column width with invalid sheet name + assert.EqualError(t, f.SetColWidth("Sheet:1", "A", "D", 13), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2)) assert.NoError(t, f.SetRowOutlineLevel("Sheet1", 2, 7)) assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "D", 8), ErrOutlineLevel.Error()) assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 2, 8), ErrOutlineLevel.Error()) - // Test set row outline level on not exists worksheet. + // Test set row outline level on not exists worksheet assert.EqualError(t, f.SetRowOutlineLevel("SheetN", 1, 4), "sheet SheetN does not exist") - // Test get row outline level on not exists worksheet. + // Test set row outline level with invalid sheet name + assert.EqualError(t, f.SetRowOutlineLevel("Sheet:1", 1, 4), ErrSheetNameInvalid.Error()) + // Test get row outline level on not exists worksheet _, err = f.GetRowOutlineLevel("SheetN", 1) assert.EqualError(t, err, "sheet SheetN does not exist") - - // Test set and get column outline level with illegal cell reference. + // Test get row outline level with invalid sheet name + _, err = f.GetRowOutlineLevel("Sheet:1", 1) + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + // Test set and get column outline level with illegal cell reference assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), newInvalidColumnNameError("*").Error()) _, err = f.GetColOutlineLevel("Sheet1", "*") assert.EqualError(t, err, newInvalidColumnNameError("*").Error()) - // Test set column outline level on not exists worksheet. + // Test set column outline level on not exists worksheet assert.EqualError(t, f.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN does not exist") assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), newInvalidRowNumberError(0).Error()) @@ -300,22 +320,24 @@ func TestSetColStyle(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", "B2", "Hello")) styleID, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#94d3a2"],"pattern":1}}`) assert.NoError(t, err) - // Test set column style on not exists worksheet. + // Test set column style on not exists worksheet assert.EqualError(t, f.SetColStyle("SheetN", "E", styleID), "sheet SheetN does not exist") - // Test set column style with illegal column name. + // Test set column style with illegal column name assert.EqualError(t, f.SetColStyle("Sheet1", "*", styleID), newInvalidColumnNameError("*").Error()) assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", styleID), newInvalidColumnNameError("*").Error()) - // Test set column style with invalid style ID. + // Test set column style with invalid style ID assert.EqualError(t, f.SetColStyle("Sheet1", "B", -1), newInvalidStyleID(-1).Error()) - // Test set column style with not exists style ID. + // Test set column style with not exists style ID assert.EqualError(t, f.SetColStyle("Sheet1", "B", 10), newInvalidStyleID(10).Error()) + // Test set column style with invalid sheet name + assert.EqualError(t, f.SetColStyle("Sheet:1", "A", 0), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SetColStyle("Sheet1", "B", styleID)) style, err := f.GetColStyle("Sheet1", "B") assert.NoError(t, err) assert.Equal(t, styleID, style) - // Test set column style with already exists column with style. + // Test set column style with already exists column with style assert.NoError(t, f.SetColStyle("Sheet1", "B", styleID)) assert.NoError(t, f.SetColStyle("Sheet1", "D:C", styleID)) ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") @@ -325,7 +347,7 @@ func TestSetColStyle(t *testing.T) { assert.NoError(t, err) assert.Equal(t, styleID, cellStyleID) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetColStyle.xlsx"))) - // Test set column style with unsupported charset style sheet. + // Test set column style with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) assert.EqualError(t, f.SetColStyle("Sheet1", "C:F", styleID), "XML syntax error on line 1: invalid UTF-8") @@ -342,19 +364,21 @@ func TestColWidth(t *testing.T) { assert.Equal(t, defaultColWidth, width) assert.NoError(t, err) - // Test set and get column width with illegal cell reference. + // Test set and get column width with illegal cell reference width, err = f.GetColWidth("Sheet1", "*") assert.Equal(t, defaultColWidth, width) assert.EqualError(t, err, newInvalidColumnNameError("*").Error()) assert.EqualError(t, f.SetColWidth("Sheet1", "*", "B", 1), newInvalidColumnNameError("*").Error()) assert.EqualError(t, f.SetColWidth("Sheet1", "A", "*", 1), newInvalidColumnNameError("*").Error()) - // Test set column width on not exists worksheet. + // Test set column width on not exists worksheet assert.EqualError(t, f.SetColWidth("SheetN", "B", "A", 12), "sheet SheetN does not exist") - - // Test get column width on not exists worksheet. + // Test get column width on not exists worksheet _, err = f.GetColWidth("SheetN", "A") assert.EqualError(t, err, "sheet SheetN does not exist") + // Test get column width invalid sheet name + _, err = f.GetColWidth("Sheet:1", "A") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColWidth.xlsx"))) convertRowHeightToPixels(0) @@ -366,12 +390,15 @@ func TestGetColStyle(t *testing.T) { assert.NoError(t, err) assert.Equal(t, styleID, 0) - // Test set column style on not exists worksheet. + // Test get column style on not exists worksheet _, err = f.GetColStyle("SheetN", "A") assert.EqualError(t, err, "sheet SheetN does not exist") - // Test set column style with illegal column name. + // Test get column style with illegal column name _, err = f.GetColStyle("Sheet1", "*") assert.EqualError(t, err, newInvalidColumnNameError("*").Error()) + // Test get column style with invalid sheet name + _, err = f.GetColStyle("Sheet:1", "A") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) } func TestInsertCols(t *testing.T) { @@ -386,9 +413,10 @@ func TestInsertCols(t *testing.T) { assert.NoError(t, f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`)) assert.NoError(t, f.InsertCols(sheet1, "A", 1)) - // Test insert column with illegal cell reference. + // Test insert column with illegal cell reference assert.EqualError(t, f.InsertCols(sheet1, "*", 1), newInvalidColumnNameError("*").Error()) - + // Test insert column with invalid sheet name + assert.EqualError(t, f.InsertCols("Sheet:1", "A", 1), ErrSheetNameInvalid.Error()) assert.EqualError(t, f.InsertCols(sheet1, "A", 0), ErrColumnNumber.Error()) assert.EqualError(t, f.InsertCols(sheet1, "A", MaxColumns), ErrColumnNumber.Error()) assert.EqualError(t, f.InsertCols(sheet1, "A", MaxColumns-10), ErrColumnNumber.Error()) @@ -411,11 +439,12 @@ func TestRemoveCol(t *testing.T) { assert.NoError(t, f.RemoveCol(sheet1, "A")) assert.NoError(t, f.RemoveCol(sheet1, "A")) - // Test remove column with illegal cell reference. + // Test remove column with illegal cell reference assert.EqualError(t, f.RemoveCol("Sheet1", "*"), newInvalidColumnNameError("*").Error()) - - // Test remove column on not exists worksheet. + // Test remove column on not exists worksheet assert.EqualError(t, f.RemoveCol("SheetN", "B"), "sheet SheetN does not exist") + // Test remove column with invalid sheet name + assert.EqualError(t, f.RemoveCol("Sheet:1", "A"), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } diff --git a/comment.go b/comment.go index ae62c37858..395d7c1acd 100644 --- a/comment.go +++ b/comment.go @@ -143,6 +143,9 @@ func (f *File) AddComment(sheet string, comment Comment) error { // // err := f.DeleteComment("Sheet1", "A30") func (f *File) DeleteComment(sheet, cell string) error { + if err := checkSheetName(sheet); err != nil { + return err + } sheetXMLPath, ok := f.getSheetXMLPath(sheet) if !ok { return newNoExistSheetError(sheet) diff --git a/comment_test.go b/comment_test.go index ead393945c..0f668f1bff 100644 --- a/comment_test.go +++ b/comment_test.go @@ -20,7 +20,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAddComments(t *testing.T) { +func TestAddComment(t *testing.T) { f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() @@ -30,7 +30,7 @@ func TestAddComments(t *testing.T) { assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A30", Author: s, Text: s, Runs: []RichTextRun{{Text: s}, {Text: s}}})) assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "B7", Author: "Excelize", Text: s[:TotalCellChars-1], Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}})) - // Test add comment on not exists worksheet. + // Test add comment on not exists worksheet assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist") // Test add comment on with illegal cell reference assert.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) @@ -50,18 +50,21 @@ func TestAddComments(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, len(comments), 0) - // Test add comments with unsupported charset. + // Test add comments with invalid sheet name + assert.EqualError(t, f.AddComment("Sheet:1", Comment{Cell: "A1", Author: "Excelize", Text: "This is a comment."}), ErrSheetNameInvalid.Error()) + + // Test add comments with unsupported charset f.Comments["xl/comments2.xml"] = nil f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset) _, err = f.GetComments() assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") - // Test add comments with unsupported charset. + // Test add comments with unsupported charset f.Comments["xl/comments2.xml"] = nil f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset) assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8") - // Test add comments with unsupported charset style sheet. + // Test add comments with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8") @@ -90,6 +93,8 @@ func TestDeleteComment(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, len(comments), 0) + // Test delete comment with invalid sheet name + assert.EqualError(t, f.DeleteComment("Sheet:1", "A1"), ErrSheetNameInvalid.Error()) // Test delete all comments in a worksheet assert.NoError(t, f.DeleteComment("Sheet2", "A41")) assert.NoError(t, f.DeleteComment("Sheet2", "C41")) @@ -118,7 +123,7 @@ func TestDecodeVMLDrawingReader(t *testing.T) { func TestCommentsReader(t *testing.T) { f := NewFile() - // Test read comments with unsupported charset. + // Test read comments with unsupported charset path := "xl/comments1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) _, err := f.commentsReader(path) diff --git a/datavalidation_test.go b/datavalidation_test.go index c0d91177eb..c307d20187 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -91,6 +91,9 @@ func TestDataValidation(t *testing.T) { // Test get data validation on no exists worksheet _, err = f.GetDataValidations("SheetN") assert.EqualError(t, err, "sheet SheetN does not exist") + // Test get data validation with invalid sheet name + _, err = f.GetDataValidations("Sheet:1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.NoError(t, f.SaveAs(resultFile)) @@ -130,7 +133,7 @@ func TestDataValidationError(t *testing.T) { assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) - // Test width invalid data validation formula. + // Test width invalid data validation formula prevFormula1 := dvRange.Formula1 for _, keys := range [][]string{ make([]string, 257), @@ -156,9 +159,13 @@ func TestDataValidationError(t *testing.T) { DataValidationTypeWhole, DataValidationOperatorGreaterThan), ErrDataValidationRange.Error()) assert.NoError(t, f.SaveAs(resultFile)) - // Test add data validation on no exists worksheet. + // Test add data validation on no exists worksheet f = NewFile() assert.EqualError(t, f.AddDataValidation("SheetN", nil), "sheet SheetN does not exist") + + // Test add data validation with invalid sheet name + f = NewFile() + assert.EqualError(t, f.AddDataValidation("Sheet:1", nil), ErrSheetNameInvalid.Error()) } func TestDeleteDataValidation(t *testing.T) { @@ -200,10 +207,11 @@ func TestDeleteDataValidation(t *testing.T) { ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A1:A" assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1:B2"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - // Test delete data validation on no exists worksheet. + // Test delete data validation on no exists worksheet assert.EqualError(t, f.DeleteDataValidation("SheetN", "A1:B2"), "sheet SheetN does not exist") - - // Test delete all data validations in the worksheet. + // Test delete all data validation with invalid sheet name + assert.EqualError(t, f.DeleteDataValidation("Sheet:1"), ErrSheetNameInvalid.Error()) + // Test delete all data validations in the worksheet assert.NoError(t, f.DeleteDataValidation("Sheet1")) assert.Nil(t, ws.(*xlsxWorksheet).DataValidations) } diff --git a/errors.go b/errors.go index 1f7c6f8419..7a31a4cd0a 100644 --- a/errors.go +++ b/errors.go @@ -113,9 +113,8 @@ var ( // ErrCoordinates defined the error message on invalid coordinates tuples // length. ErrCoordinates = errors.New("coordinates length must be 4") - // ErrExistsWorksheet defined the error message on given worksheet already - // exists. - ErrExistsWorksheet = errors.New("the same name worksheet already exists") + // ErrExistsSheet defined the error message on given sheet already exists. + ErrExistsSheet = errors.New("the same name sheet already exists") // ErrTotalSheetHyperlinks defined the error message on hyperlinks count // overflow. ErrTotalSheetHyperlinks = errors.New("over maximum limit hyperlinks in a worksheet") @@ -219,4 +218,16 @@ var ( // ErrWorkbookPassword defined the error message on receiving the incorrect // workbook password. ErrWorkbookPassword = errors.New("the supplied open workbook password is not correct") + // ErrSheetNameInvalid defined the error message on receive the sheet name + // contains invalid characters. + ErrSheetNameInvalid = errors.New("the sheet can not contain any of the characters :\\/?*[or]") + // ErrSheetNameSingleQuote defined the error message on the first or last + // character of the sheet name was a single quote. + ErrSheetNameSingleQuote = errors.New("the first or last character of the sheet name can not be a single quote") + // ErrSheetNameBlank defined the error message on receive the blank sheet + // name. + ErrSheetNameBlank = errors.New("the sheet name can not be blank") + // ErrSheetNameLength defined the error message on receiving the sheet + // name length exceeds the limit. + ErrSheetNameLength = fmt.Errorf("the sheet name length exceeds the %d characters limit", MaxSheetNameLength) ) diff --git a/excelize.go b/excelize.go index f4c7a255a8..f84afd6f29 100644 --- a/excelize.go +++ b/excelize.go @@ -233,6 +233,9 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { name string ok bool ) + if err = checkSheetName(sheet); err != nil { + return + } if name, ok = f.getSheetXMLPath(sheet); !ok { err = newNoExistSheetError(sheet) return diff --git a/excelize_test.go b/excelize_test.go index ece74b2f4b..1dad0ff541 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -23,14 +23,17 @@ import ( ) func TestOpenFile(t *testing.T) { - // Test update the spreadsheet file. + // Test update the spreadsheet file f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) - // Test get all the rows in a not exists worksheet. + // Test get all the rows in a not exists worksheet _, err = f.GetRows("Sheet4") assert.EqualError(t, err, "sheet Sheet4 does not exist") - // Test get all the rows in a worksheet. + // Test get all the rows with invalid sheet name + _, err = f.GetRows("Sheet:1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + // Test get all the rows in a worksheet rows, err := f.GetRows("Sheet2") expected := [][]string{ {"Monitor", "", "Brand", "", "inlineStr"}, @@ -52,40 +55,44 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(100.1588, 'f', -1, 32))) assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(-100.1588, 'f', -1, 64))) - - // Test set cell value with illegal row number. + // Test set cell value with invalid sheet name + assert.EqualError(t, f.SetCellDefault("Sheet:1", "A1", ""), ErrSheetNameInvalid.Error()) + // Test set cell value with illegal row number assert.EqualError(t, f.SetCellDefault("Sheet2", "A", strconv.FormatFloat(-100.1588, 'f', -1, 64)), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.SetCellInt("Sheet2", "A1", 100)) - // Test set cell integer value with illegal row number. + // Test set cell integer value with illegal row number assert.EqualError(t, f.SetCellInt("Sheet2", "A", 100), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + // Test set cell integer value with invalid sheet name + assert.EqualError(t, f.SetCellInt("Sheet:1", "A1", 100), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SetCellStr("Sheet2", "C11", "Knowns")) - // Test max characters in a cell. + // Test max characters in a cell assert.NoError(t, f.SetCellStr("Sheet2", "D11", strings.Repeat("c", TotalCellChars+2))) f.NewSheet(":\\/?*[]Maximum 31 characters allowed in sheet title.") - // Test set worksheet name with illegal name. + // Test set worksheet name with illegal name f.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title.") assert.EqualError(t, f.SetCellInt("Sheet3", "A23", 10), "sheet Sheet3 does not exist") assert.EqualError(t, f.SetCellStr("Sheet3", "b230", "10"), "sheet Sheet3 does not exist") assert.EqualError(t, f.SetCellStr("Sheet10", "b230", "10"), "sheet Sheet10 does not exist") - - // Test set cell string value with illegal row number. + // Test set cell string data type value with invalid sheet name + assert.EqualError(t, f.SetCellStr("Sheet:1", "A1", "1"), ErrSheetNameInvalid.Error()) + // Test set cell string value with illegal row number assert.EqualError(t, f.SetCellStr("Sheet1", "A", "10"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) f.SetActiveSheet(2) - // Test get cell formula with given rows number. + // Test get cell formula with given rows number _, err = f.GetCellFormula("Sheet1", "B19") assert.NoError(t, err) - // Test get cell formula with illegal worksheet name. + // Test get cell formula with illegal worksheet name _, err = f.GetCellFormula("Sheet2", "B20") assert.NoError(t, err) _, err = f.GetCellFormula("Sheet1", "B20") assert.NoError(t, err) - // Test get cell formula with illegal rows number. + // Test get cell formula with illegal rows number _, err = f.GetCellFormula("Sheet1", "B") assert.EqualError(t, err, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) // Test get shared cell formula @@ -110,7 +117,7 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, err) _, err = f.GetCellValue("Sheet2", "D12") assert.NoError(t, err) - // Test SetCellValue function. + // Test SetCellValue function assert.NoError(t, f.SetCellValue("Sheet2", "F1", " Hello")) assert.NoError(t, f.SetCellValue("Sheet2", "G1", []byte("World"))) assert.NoError(t, f.SetCellValue("Sheet2", "F2", 42)) @@ -130,7 +137,7 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet2", "F16", true)) assert.NoError(t, f.SetCellValue("Sheet2", "F17", complex64(5+10i))) - // Test on not exists worksheet. + // Test on not exists worksheet assert.EqualError(t, f.SetCellDefault("SheetN", "A1", ""), "sheet SheetN does not exist") assert.EqualError(t, f.SetCellFloat("SheetN", "A1", 42.65418, 2, 32), "sheet SheetN does not exist") assert.EqualError(t, f.SetCellBool("SheetN", "A1", true), "sheet SheetN does not exist") @@ -163,18 +170,18 @@ func TestOpenFile(t *testing.T) { assert.EqualError(t, f.SetCellValue("SheetN", "A1", time.Now()), "sheet SheetN does not exist") // 02:46:40 assert.NoError(t, f.SetCellValue("Sheet2", "G5", time.Duration(1e13))) - // Test completion column. + // Test completion column assert.NoError(t, f.SetCellValue("Sheet2", "M2", nil)) - // Test read cell value with given cell reference large than exists row. + // Test read cell value with given cell reference large than exists row _, err = f.GetCellValue("Sheet2", "E231") assert.NoError(t, err) - // Test get active worksheet of spreadsheet and get worksheet name of spreadsheet by given worksheet index. + // Test get active worksheet of spreadsheet and get worksheet name of spreadsheet by given worksheet index f.GetSheetName(f.GetActiveSheetIndex()) - // Test get worksheet index of spreadsheet by given worksheet name. + // Test get worksheet index of spreadsheet by given worksheet name f.GetSheetIndex("Sheet1") - // Test get worksheet name of spreadsheet by given invalid worksheet index. + // Test get worksheet name of spreadsheet by given invalid worksheet index f.GetSheetName(4) - // Test get worksheet map of workbook. + // Test get worksheet map of workbook f.GetSheetMap() for i := 1; i <= 300; i++ { assert.NoError(t, f.SetCellStr("Sheet2", "c"+strconv.Itoa(i), strconv.Itoa(i))) @@ -202,7 +209,7 @@ func TestSaveFile(t *testing.T) { func TestSaveAsWrongPath(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) - // Test write file to not exist directory. + // Test write file to not exist directory assert.Error(t, f.SaveAs(filepath.Join("x", "Book1.xlsx"))) assert.NoError(t, f.Close()) } @@ -218,7 +225,7 @@ func TestOpenReader(t *testing.T) { _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password", UnzipXMLSizeLimit: UnzipSizeLimit + 1}) assert.EqualError(t, err, ErrWorkbookFileFormat.Error()) - // Test open workbook with unsupported charset internal calculation chain. + // Test open workbook with unsupported charset internal calculation chain preset := func(filePath string) *bytes.Buffer { source, err := zip.OpenReader(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) @@ -245,11 +252,11 @@ func TestOpenReader(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } - // Test open spreadsheet with unzip size limit. + // Test open spreadsheet with unzip size limit _, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipSizeLimit: 100}) assert.EqualError(t, err, newUnzipSizeLimitError(100).Error()) - // Test open password protected spreadsheet created by Microsoft Office Excel 2010. + // Test open password protected spreadsheet created by Microsoft Office Excel 2010 f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"}) assert.NoError(t, err) val, err := f.GetCellValue("Sheet1", "A1") @@ -257,7 +264,7 @@ func TestOpenReader(t *testing.T) { assert.Equal(t, "SECRET", val) assert.NoError(t, f.Close()) - // Test open password protected spreadsheet created by LibreOffice 7.0.0.3. + // Test open password protected spreadsheet created by LibreOffice 7.0.0.3 f, err = OpenFile(filepath.Join("test", "encryptAES.xlsx"), Options{Password: "password"}) assert.NoError(t, err) val, err = f.GetCellValue("Sheet1", "A1") @@ -265,11 +272,11 @@ func TestOpenReader(t *testing.T) { assert.Equal(t, "SECRET", val) assert.NoError(t, f.Close()) - // Test open spreadsheet with invalid options. + // Test open spreadsheet with invalid options _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{UnzipSizeLimit: 1, UnzipXMLSizeLimit: 2}) assert.EqualError(t, err, ErrOptionsUnzipSizeLimit.Error()) - // Test unexpected EOF. + // Test unexpected EOF var b bytes.Buffer w := gzip.NewWriter(&b) defer w.Close() @@ -299,7 +306,7 @@ func TestOpenReader(t *testing.T) { } func TestBrokenFile(t *testing.T) { - // Test write file with broken file struct. + // Test write file with broken file struct f := File{} t.Run("SaveWithoutName", func(t *testing.T) { @@ -307,12 +314,12 @@ func TestBrokenFile(t *testing.T) { }) t.Run("SaveAsEmptyStruct", func(t *testing.T) { - // Test write file with broken file struct with given path. + // Test write file with broken file struct with given path assert.NoError(t, f.SaveAs(filepath.Join("test", "BadWorkbook.SaveAsEmptyStruct.xlsx"))) }) t.Run("OpenBadWorkbook", func(t *testing.T) { - // Test set active sheet without BookViews and Sheets maps in xl/workbook.xml. + // Test set active sheet without BookViews and Sheets maps in xl/workbook.xml f3, err := OpenFile(filepath.Join("test", "BadWorkbook.xlsx")) f3.GetActiveSheetIndex() f3.SetActiveSheet(1) @@ -321,7 +328,7 @@ func TestBrokenFile(t *testing.T) { }) t.Run("OpenNotExistsFile", func(t *testing.T) { - // Test open a spreadsheet file with given illegal path. + // Test open a spreadsheet file with given illegal path _, err := OpenFile(filepath.Join("test", "NotExistsFile.xlsx")) if assert.Error(t, err) { assert.True(t, os.IsNotExist(err), "Expected os.IsNotExists(err) == true") @@ -330,7 +337,7 @@ func TestBrokenFile(t *testing.T) { } func TestNewFile(t *testing.T) { - // Test create a spreadsheet file. + // Test create a spreadsheet file f := NewFile() f.NewSheet("Sheet1") f.NewSheet("XLSXSheet2") @@ -339,20 +346,20 @@ func TestNewFile(t *testing.T) { assert.NoError(t, f.SetCellStr("Sheet1", "B20", "42")) f.SetActiveSheet(0) - // Test add picture to sheet with scaling and positioning. + // Test add picture to sheet with scaling and positioning err := f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), `{"x_scale": 0.5, "y_scale": 0.5, "positioning": "absolute"}`) if !assert.NoError(t, err) { t.FailNow() } - // Test add picture to worksheet without options. + // Test add picture to worksheet without options err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), "") if !assert.NoError(t, err) { t.FailNow() } - // Test add picture to worksheet with invalid options. + // Test add picture to worksheet with invalid options err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), `{`) if !assert.Error(t, err) { t.FailNow() @@ -363,7 +370,7 @@ func TestNewFile(t *testing.T) { } func TestAddDrawingVML(t *testing.T) { - // Test addDrawingVML with illegal cell reference. + // Test addDrawingVML with illegal cell reference f := NewFile() assert.EqualError(t, f.addDrawingVML(0, "", "*", 0, 0), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error()) @@ -374,23 +381,22 @@ func TestAddDrawingVML(t *testing.T) { func TestSetCellHyperLink(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) - // Test set cell hyperlink in a work sheet already have hyperlinks. + // Test set cell hyperlink in a work sheet already have hyperlinks assert.NoError(t, f.SetCellHyperLink("Sheet1", "B19", "https://github.com/xuri/excelize", "External")) - // Test add first hyperlink in a work sheet. + // Test add first hyperlink in a work sheet assert.NoError(t, f.SetCellHyperLink("Sheet2", "C1", "https://github.com/xuri/excelize", "External")) // Test add Location hyperlink in a work sheet. assert.NoError(t, f.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location")) - // Test add Location hyperlink with display & tooltip in a work sheet. + // Test add Location hyperlink with display & tooltip in a work sheet display, tooltip := "Display value", "Hover text" assert.NoError(t, f.SetCellHyperLink("Sheet2", "D7", "Sheet1!D9", "Location", HyperlinkOpts{ Display: &display, Tooltip: &tooltip, })) - + // Test set cell hyperlink with invalid sheet name + assert.EqualError(t, f.SetCellHyperLink("Sheet:1", "A1", "Sheet1!D60", "Location"), ErrSheetNameInvalid.Error()) assert.EqualError(t, f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""), `invalid link type ""`) - assert.EqualError(t, f.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location"), `invalid cell name ""`) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) assert.NoError(t, f.Close()) @@ -467,6 +473,10 @@ func TestGetCellHyperLink(t *testing.T) { assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, link, false) assert.Equal(t, target, "") + + // Test get cell hyperlink with invalid sheet name + _, _, err = f.GetCellHyperLink("Sheet:1", "A1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) } func TestSetSheetBackground(t *testing.T) { @@ -489,12 +499,14 @@ func TestSetSheetBackgroundErrors(t *testing.T) { err = f.SetSheetBackground("Sheet2", filepath.Join("test", "Book1.xlsx")) assert.EqualError(t, err, ErrImgExt.Error()) - // Test set sheet background on not exist worksheet. + // Test set sheet background on not exist worksheet err = f.SetSheetBackground("SheetN", filepath.Join("test", "images", "background.jpg")) assert.EqualError(t, err, "sheet SheetN does not exist") + // Test set sheet background with invalid sheet name + assert.EqualError(t, f.SetSheetBackground("Sheet:1", filepath.Join("test", "images", "background.jpg")), ErrSheetNameInvalid.Error()) assert.NoError(t, f.Close()) - // Test set sheet background with unsupported charset content types. + // Test set sheet background with unsupported charset content types f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) @@ -510,7 +522,6 @@ func TestWriteArrayFormula(t *testing.T) { if err != nil { t.Fatal(err) } - return c } @@ -621,15 +632,20 @@ func TestSetCellStyleAlignment(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet1", "A22", "A22", style)) - // Test set cell style with given illegal rows number. + // Test set cell style with given illegal rows number assert.EqualError(t, f.SetCellStyle("Sheet1", "A", "A22", style), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.SetCellStyle("Sheet1", "A22", "A", style), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - - // Test get cell style with given illegal rows number. + // Test set cell style with invalid sheet name + assert.EqualError(t, f.SetCellStyle("Sheet:1", "A1", "A2", style), ErrSheetNameInvalid.Error()) + // Test get cell style with given illegal rows number index, err := f.GetCellStyle("Sheet1", "A") assert.Equal(t, 0, index) assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + // Test get cell style with invalid sheet name + _, err = f.GetCellStyle("Sheet:1", "A1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleAlignment.xlsx"))) } @@ -987,18 +1003,18 @@ func TestSheetVisibility(t *testing.T) { assert.NoError(t, f.SetSheetVisible("Sheet2", false)) assert.NoError(t, f.SetSheetVisible("Sheet1", false)) assert.NoError(t, f.SetSheetVisible("Sheet1", true)) - assert.Equal(t, true, f.GetSheetVisible("Sheet1")) - + visible, err := f.GetSheetVisible("Sheet1") + assert.Equal(t, true, visible) + assert.NoError(t, err) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSheetVisibility.xlsx"))) } func TestCopySheet(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) - idx := f.NewSheet("CopySheet") + idx, err := f.NewSheet("CopySheet") + assert.NoError(t, err) assert.NoError(t, f.CopySheet(0, idx)) assert.NoError(t, f.SetCellValue("CopySheet", "F1", "Hello")) @@ -1011,15 +1027,9 @@ func TestCopySheet(t *testing.T) { func TestCopySheetError(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - assert.EqualError(t, f.copySheet(-1, -2), "sheet does not exist") - if !assert.EqualError(t, f.CopySheet(-1, -2), "invalid worksheet index") { - t.FailNow() - } - + assert.NoError(t, err) + assert.EqualError(t, f.copySheet(-1, -2), ErrSheetNameBlank.Error()) + assert.EqualError(t, f.CopySheet(-1, -2), ErrSheetIdx.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestCopySheetError.xlsx"))) } @@ -1072,9 +1082,9 @@ func TestConditionalFormat(t *testing.T) { t.FailNow() } - // Color scales: 2 color. + // Color scales: 2 color assert.NoError(t, f.SetConditionalFormat(sheet1, "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`)) - // Color scales: 3 color. + // Color scales: 3 color assert.NoError(t, f.SetConditionalFormat(sheet1, "B1:B10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`)) // Highlight cells rules: between... assert.NoError(t, f.SetConditionalFormat(sheet1, "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1))) @@ -1092,29 +1102,31 @@ func TestConditionalFormat(t *testing.T) { assert.NoError(t, f.SetConditionalFormat(sheet1, "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3))) // Top/Bottom rules: Below Average... assert.NoError(t, f.SetConditionalFormat(sheet1, "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1))) - // Data Bars: Gradient Fill. + // Data Bars: Gradient Fill assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`)) - // Use a formula to determine which cells to format. + // Use a formula to determine which cells to format assert.NoError(t, f.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1))) - // Alignment/Border cells rules. + // Alignment/Border cells rules assert.NoError(t, f.SetConditionalFormat(sheet1, "M1:M10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"0"}]`, format4))) - // Test set invalid format set in conditional format. + // Test set invalid format set in conditional format assert.EqualError(t, f.SetConditionalFormat(sheet1, "L1:L10", ""), "unexpected end of JSON input") - // Set conditional format on not exists worksheet. + // Test set conditional format on not exists worksheet assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", "[]"), "sheet SheetN does not exist") + // Test set conditional format with invalid sheet name + assert.EqualError(t, f.SetConditionalFormat("Sheet:1", "L1:L10", "[]"), ErrSheetNameInvalid.Error()) err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - // Set conditional format with illegal valid type. + // Set conditional format with illegal valid type assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`)) - // Set conditional format with illegal criteria type. + // Set conditional format with illegal criteria type assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`)) - // Set conditional format with file without dxfs element should not return error. + // Set conditional format with file without dxfs element should not return error f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() @@ -1168,7 +1180,8 @@ func TestSetSheetCol(t *testing.T) { assert.EqualError(t, f.SetSheetCol("Sheet1", "", &[]interface{}{"cell", nil, 2}), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) - + // Test set worksheet column values with invalid sheet name + assert.EqualError(t, f.SetSheetCol("Sheet:1", "A1", &[]interface{}{nil}), ErrSheetNameInvalid.Error()) assert.EqualError(t, f.SetSheetCol("Sheet1", "B27", []interface{}{}), ErrParameterInvalid.Error()) assert.EqualError(t, f.SetSheetCol("Sheet1", "B27", &f), ErrParameterInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetCol.xlsx"))) @@ -1185,7 +1198,8 @@ func TestSetSheetRow(t *testing.T) { assert.EqualError(t, f.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) - + // Test set worksheet row with invalid sheet name + assert.EqualError(t, f.SetSheetRow("Sheet:1", "A1", &[]interface{}{1}), ErrSheetNameInvalid.Error()) assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", []interface{}{}), ErrParameterInvalid.Error()) assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", &f), ErrParameterInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) @@ -1261,6 +1275,8 @@ func TestProtectSheet(t *testing.T) { assert.Equal(t, int(sheetProtectionSpinCount), ws.SheetProtection.SpinCount) // Test remove sheet protection with an incorrect password assert.EqualError(t, f.UnprotectSheet(sheetName, "wrongPassword"), ErrUnprotectSheetPassword.Error()) + // Test remove sheet protection with invalid sheet name + assert.EqualError(t, f.UnprotectSheet("Sheet:1", "wrongPassword"), ErrSheetNameInvalid.Error()) // Test remove sheet protection with password verification assert.NoError(t, f.UnprotectSheet(sheetName, "password")) // Test protect worksheet with empty password @@ -1276,8 +1292,10 @@ func TestProtectSheet(t *testing.T) { AlgorithmName: "RIPEMD-160", Password: "password", }), ErrUnsupportedHashAlgorithm.Error()) - // Test protect not exists worksheet. + // Test protect not exists worksheet assert.EqualError(t, f.ProtectSheet("SheetN", nil), "sheet SheetN does not exist") + // Test protect sheet with invalid sheet name + assert.EqualError(t, f.ProtectSheet("Sheet:1", nil), ErrSheetNameInvalid.Error()) } func TestUnprotectSheet(t *testing.T) { @@ -1326,10 +1344,10 @@ func TestAddVBAProject(t *testing.T) { assert.EqualError(t, f.AddVBAProject("macros.bin"), "stat macros.bin: no such file or directory") assert.EqualError(t, f.AddVBAProject(filepath.Join("test", "Book1.xlsx")), ErrAddVBAProject.Error()) assert.NoError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin"))) - // Test add VBA project twice. + // Test add VBA project twice assert.NoError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddVBAProject.xlsm"))) - // Test add VBs with unsupported charset workbook relationships. + // Test add VBA with unsupported charset workbook relationships f.Relationships.Delete(defaultXMLPathWorkbookRels) f.Pkg.Store(defaultXMLPathWorkbookRels, MacintoshCyrillicCharset) assert.EqualError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin")), "XML syntax error on line 1: invalid UTF-8") diff --git a/merge_test.go b/merge_test.go index 31f2cf4635..40055c9ccd 100644 --- a/merge_test.go +++ b/merge_test.go @@ -29,7 +29,8 @@ func TestMergeCell(t *testing.T) { value, err := f.GetCellValue("Sheet1", "H11") assert.Equal(t, "100", value) assert.NoError(t, err) - value, err = f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. + // Merged cell ref is single coordinate + value, err = f.GetCellValue("Sheet2", "A6") assert.Equal(t, "", value) assert.NoError(t, err) value, err = f.GetCellFormula("Sheet1", "G12") @@ -64,9 +65,10 @@ func TestMergeCell(t *testing.T) { assert.NoError(t, f.MergeCell("Sheet3", "M8", "Q13")) assert.NoError(t, f.MergeCell("Sheet3", "N10", "O11")) - // Test get merged cells on not exists worksheet. + // Test merge cells on not exists worksheet assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN does not exist") - + // Test merged cells with invalid sheet name + assert.EqualError(t, f.MergeCell("Sheet:1", "N10", "O11"), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) assert.NoError(t, f.Close()) @@ -137,8 +139,10 @@ func TestGetMergeCells(t *testing.T) { assert.Equal(t, wants[i].start, m.GetStartAxis()) assert.Equal(t, wants[i].end, m.GetEndAxis()) } - - // Test get merged cells on not exists worksheet. + // Test get merged cells with invalid sheet name + _, err = f.GetMergeCells("Sheet:1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + // Test get merged cells on not exists worksheet _, err = f.GetMergeCells("SheetN") assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.Close()) @@ -158,7 +162,7 @@ func TestUnmergeCell(t *testing.T) { assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - // unmerge the mergecell that contains A1 + // Test unmerge the merged cells that contains A1 assert.NoError(t, f.UnmergeCell(sheet1, "A1", "A1")) if len(sheet.MergeCells.Cells) != mergeCellNum-1 { t.FailNow() @@ -169,9 +173,12 @@ func TestUnmergeCell(t *testing.T) { f = NewFile() assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) - // Test unmerged range reference on not exists worksheet. + // Test unmerged range reference on not exists worksheet assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN does not exist") + // Test unmerge the merged cells with invalid sheet name + assert.EqualError(t, f.UnmergeCell("Sheet:1", "A1", "A1"), ErrSheetNameInvalid.Error()) + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).MergeCells = nil diff --git a/picture_test.go b/picture_test.go index 11196c6642..23923142fb 100644 --- a/picture_test.go +++ b/picture_test.go @@ -35,17 +35,17 @@ func TestAddPicture(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) - // Test add picture to worksheet with offset and location hyperlink. + // Test add picture to worksheet with offset and location hyperlink assert.NoError(t, f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`)) - // Test add picture to worksheet with offset, external hyperlink and positioning. + // Test add picture to worksheet with offset, external hyperlink and positioning assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)) file, err := os.ReadFile(filepath.Join("test", "images", "excel.png")) assert.NoError(t, err) - // Test add picture to worksheet with autofit. + // Test add picture to worksheet with autofit assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`)) assert.NoError(t, f.AddPicture("Sheet1", "B30", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 10, "y_offset": 10, "autofit": true}`)) f.NewSheet("AddPicture") @@ -55,41 +55,44 @@ func TestAddPicture(t *testing.T) { assert.NoError(t, f.AddPicture("AddPicture", "C6", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`)) assert.NoError(t, f.AddPicture("AddPicture", "A1", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`)) - // Test add picture to worksheet from bytes. + // Test add picture to worksheet from bytes assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file)) - // Test add picture to worksheet from bytes with illegal cell reference. + // Test add picture to worksheet from bytes with illegal cell reference assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", "", "Excel Logo", ".png", file), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.AddPicture("Sheet1", "Q8", filepath.Join("test", "images", "excel.gif"), "")) assert.NoError(t, f.AddPicture("Sheet1", "Q15", filepath.Join("test", "images", "excel.jpg"), "")) assert.NoError(t, f.AddPicture("Sheet1", "Q22", filepath.Join("test", "images", "excel.tif"), "")) - // Test write file to given path. + // Test write file to given path assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx"))) assert.NoError(t, f.Close()) - // Test add picture with unsupported charset content types. + // Test add picture with unsupported charset content types f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file), "XML syntax error on line 1: invalid UTF-8") + + // Test add picture with invalid sheet name + assert.EqualError(t, f.AddPicture("Sheet:1", "A1", filepath.Join("test", "images", "excel.jpg"), ""), ErrSheetNameInvalid.Error()) } func TestAddPictureErrors(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) - // Test add picture to worksheet with invalid file path. + // Test add picture to worksheet with invalid file path assert.Error(t, f.AddPicture("Sheet1", "G21", filepath.Join("test", "not_exists_dir", "not_exists.icon"), "")) - // Test add picture to worksheet with unsupported file type. + // Test add picture to worksheet with unsupported file type assert.EqualError(t, f.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), ""), ErrImgExt.Error()) assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)), ErrImgExt.Error()) - // Test add picture to worksheet with invalid file data. + // Test add picture to worksheet with invalid file data assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)), image.ErrFormat.Error()) - // Test add picture with custom image decoder and encoder. + // Test add picture with custom image decoder and encoder decode := func(r io.Reader) (image.Image, error) { return nil, nil } decodeConfig := func(r io.Reader) (image.Config, error) { return image.Config{Height: 100, Width: 90}, nil } image.RegisterFormat("emf", "", decode, decodeConfig) @@ -126,22 +129,26 @@ func TestGetPicture(t *testing.T) { t.FailNow() } - // Try to get picture from a worksheet with illegal cell reference. + // Try to get picture from a worksheet with illegal cell reference _, _, err = f.GetPicture("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - // Try to get picture from a worksheet that doesn't contain any images. + // Try to get picture from a worksheet that doesn't contain any images file, raw, err = f.GetPicture("Sheet3", "I9") assert.EqualError(t, err, "sheet Sheet3 does not exist") assert.Empty(t, file) assert.Empty(t, raw) - // Try to get picture from a cell that doesn't contain an image. + // Try to get picture from a cell that doesn't contain an image file, raw, err = f.GetPicture("Sheet2", "A2") assert.NoError(t, err) assert.Empty(t, file) assert.Empty(t, raw) + // Test get picture with invalid sheet name + _, _, err = f.GetPicture("Sheet:1", "A2") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + f.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8") f.getDrawingRelationships("", "") f.getSheetRelationshipsTargetByID("", "") @@ -160,14 +167,14 @@ func TestGetPicture(t *testing.T) { t.FailNow() } - // Try to get picture from a local storage file that doesn't contain an image. + // Try to get picture from a local storage file that doesn't contain an image file, raw, err = f.GetPicture("Sheet1", "F22") assert.NoError(t, err) assert.Empty(t, file) assert.Empty(t, raw) assert.NoError(t, f.Close()) - // Test get picture from none drawing worksheet. + // Test get picture from none drawing worksheet f = NewFile() file, raw, err = f.GetPicture("Sheet1", "F22") assert.NoError(t, err) @@ -176,7 +183,7 @@ func TestGetPicture(t *testing.T) { f, err = prepareTestBook1() assert.NoError(t, err) - // Test get pictures with unsupported charset. + // Test get pictures with unsupported charset path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) _, _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels") @@ -187,7 +194,7 @@ func TestGetPicture(t *testing.T) { } func TestAddDrawingPicture(t *testing.T) { - // Test addDrawingPicture with illegal cell reference. + // Test addDrawingPicture with illegal cell reference f := NewFile() assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", "", 0, 0, image.Config{}, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) @@ -211,6 +218,8 @@ func TestAddPictureFromBytes(t *testing.T) { }) assert.Equal(t, 1, imageCount, "Duplicate image should only be stored once.") assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), "", "logo", ".png", imgFile), "sheet SheetN does not exist") + // Test add picture from bytes with invalid sheet name + assert.EqualError(t, f.AddPictureFromBytes("Sheet:1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile), ErrSheetNameInvalid.Error()) } func TestDeletePicture(t *testing.T) { @@ -220,21 +229,23 @@ func TestDeletePicture(t *testing.T) { assert.NoError(t, f.AddPicture("Sheet1", "P1", filepath.Join("test", "images", "excel.jpg"), "")) assert.NoError(t, f.DeletePicture("Sheet1", "P1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture.xlsx"))) - // Test delete picture on not exists worksheet. + // Test delete picture on not exists worksheet assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN does not exist") - // Test delete picture with invalid coordinates. + // Test delete picture with invalid sheet name + assert.EqualError(t, f.DeletePicture("Sheet:1", "A1"), ErrSheetNameInvalid.Error()) + // Test delete picture with invalid coordinates assert.EqualError(t, f.DeletePicture("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) assert.NoError(t, f.Close()) - // Test delete picture on no chart worksheet. + // Test delete picture on no chart worksheet assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1")) } func TestDrawingResize(t *testing.T) { f := NewFile() - // Test calculate drawing resize on not exists worksheet. + // Test calculate drawing resize on not exists worksheet _, _, _, _, err := f.drawingResize("SheetN", "A1", 1, 1, nil) assert.EqualError(t, err, "sheet SheetN does not exist") - // Test calculate drawing resize with invalid coordinates. + // Test calculate drawing resize with invalid coordinates _, _, _, _, err = f.drawingResize("Sheet1", "", 1, 1, nil) assert.EqualError(t, err, newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") @@ -245,7 +256,7 @@ func TestDrawingResize(t *testing.T) { func TestSetContentTypePartImageExtensions(t *testing.T) { f := NewFile() - // Test set content type part image extensions with unsupported charset content types. + // Test set content type part image extensions with unsupported charset content types f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.setContentTypePartImageExtensions(), "XML syntax error on line 1: invalid UTF-8") @@ -253,7 +264,7 @@ func TestSetContentTypePartImageExtensions(t *testing.T) { func TestSetContentTypePartVMLExtensions(t *testing.T) { f := NewFile() - // Test set content type part VML extensions with unsupported charset content types. + // Test set content type part VML extensions with unsupported charset content types f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.setContentTypePartVMLExtensions(), "XML syntax error on line 1: invalid UTF-8") @@ -261,7 +272,7 @@ func TestSetContentTypePartVMLExtensions(t *testing.T) { func TestAddContentTypePart(t *testing.T) { f := NewFile() - // Test add content type part with unsupported charset content types. + // Test add content type part with unsupported charset content types f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.addContentTypePart(0, "unknown"), "XML syntax error on line 1: invalid UTF-8") diff --git a/pivotTable_test.go b/pivotTable_test.go index fc9e09063d..206388c59b 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -225,6 +225,12 @@ func TestAddPivotTable(t *testing.T) { Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}}, })) + // Test add pivot table with invalid sheet name + assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ + DataRange: "Sheet:1!$A$1:$E$31", + PivotTableRange: "Sheet:1!$G$2:$M$34", + Rows: []PivotTableField{{Data: "Year"}}, + }), ErrSheetNameInvalid.Error()) // Test adjust range with invalid range _, _, err := f.adjustRange("") assert.EqualError(t, err, ErrParameterRequired.Error()) diff --git a/rows.go b/rows.go index 9f1ac73d57..8e0bb63f85 100644 --- a/rows.go +++ b/rows.go @@ -260,6 +260,9 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta // fmt.Println(err) // } func (f *File) Rows(sheet string) (*Rows, error) { + if err := checkSheetName(sheet); err != nil { + return nil, err + } name, ok := f.getSheetXMLPath(sheet) if !ok { return nil, ErrSheetNotExist{sheet} @@ -268,7 +271,7 @@ func (f *File) Rows(sheet string) (*Rows, error) { worksheet := ws.(*xlsxWorksheet) worksheet.Lock() defer worksheet.Unlock() - // flush data + // Flush data output, _ := xml.Marshal(worksheet) f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } diff --git a/rows_test.go b/rows_test.go index 2e49c2877b..70ad48b185 100644 --- a/rows_test.go +++ b/rows_test.go @@ -13,17 +13,15 @@ import ( func TestRows(t *testing.T) { const sheet2 = "Sheet2" - f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) - rows, err := f.Rows(sheet2) - if !assert.NoError(t, err) { - t.FailNow() - } + // Test get rows with invalid sheet name + _, err = f.Rows("Sheet:1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + rows, err := f.Rows(sheet2) + assert.NoError(t, err) var collectedRows [][]string for rows.Next() { columns, err := rows.Columns() @@ -49,13 +47,13 @@ func TestRows(t *testing.T) { _, err = f.Rows("Sheet1") assert.NoError(t, err) - // Test reload the file to memory from system temporary directory. + // Test reload the file to memory from system temporary directory f, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128}) assert.NoError(t, err) value, err := f.GetCellValue("Sheet1", "A19") assert.NoError(t, err) assert.Equal(t, "Total:", value) - // Test load shared string table to memory. + // Test load shared string table to memory err = f.SetCellValue("Sheet1", "A19", "A19") assert.NoError(t, err) value, err = f.GetCellValue("Sheet1", "A19") @@ -64,7 +62,7 @@ func TestRows(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRow.xlsx"))) assert.NoError(t, f.Close()) - // Test rows iterator with unsupported charset shared strings table. + // Test rows iterator with unsupported charset shared strings table f.SharedStrings = nil f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) rows, err = f.Rows(sheet2) @@ -154,25 +152,32 @@ func TestRowHeight(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 111.0, height) - // Test set row height overflow max row height limit. + // Test set row height overflow max row height limit assert.EqualError(t, f.SetRowHeight(sheet1, 4, MaxRowHeight+1), ErrMaxRowHeight.Error()) - // Test get row height that rows index over exists rows. + // Test get row height that rows index over exists rows height, err = f.GetRowHeight(sheet1, 5) assert.NoError(t, err) assert.Equal(t, defaultRowHeight, height) - // Test get row height that rows heights haven't changed. + // Test get row height that rows heights haven't changed height, err = f.GetRowHeight(sheet1, 3) assert.NoError(t, err) assert.Equal(t, defaultRowHeight, height) - // Test set and get row height on not exists worksheet. + // Test set and get row height on not exists worksheet assert.EqualError(t, f.SetRowHeight("SheetN", 1, 111.0), "sheet SheetN does not exist") _, err = f.GetRowHeight("SheetN", 3) assert.EqualError(t, err, "sheet SheetN does not exist") - // Test get row height with custom default row height. + // Test set row height with invalid sheet name + assert.EqualError(t, f.SetRowHeight("Sheet:1", 1, 10.0), ErrSheetNameInvalid.Error()) + + // Test get row height with invalid sheet name + _, err = f.GetRowHeight("Sheet:1", 3) + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + + // Test get row height with custom default row height assert.NoError(t, f.SetSheetProps(sheet1, &SheetPropsOptions{ DefaultRowHeight: float64Ptr(30.0), CustomHeight: boolPtr(true), @@ -181,7 +186,7 @@ func TestRowHeight(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 30.0, height) - // Test set row height with custom default row height with prepare XML. + // Test set row height with custom default row height with prepare XML assert.NoError(t, f.SetCellValue(sheet1, "A10", "A10")) f.NewSheet("Sheet2") @@ -233,17 +238,17 @@ func TestColumns(t *testing.T) { func TestSharedStringsReader(t *testing.T) { f := NewFile() - // Test read shared string with unsupported charset. + // Test read shared string with unsupported charset f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) _, err := f.sharedStringsReader() assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") - // Test read shared strings with unsupported charset content types. + // Test read shared strings with unsupported charset content types f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) _, err = f.sharedStringsReader() assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") - // Test read shared strings with unsupported charset workbook relationships. + // Test read shared strings with unsupported charset workbook relationships f = NewFile() f.Relationships.Delete(defaultXMLPathWorkbookRels) f.Pkg.Store(defaultXMLPathWorkbookRels, MacintoshCyrillicCharset) @@ -267,13 +272,17 @@ func TestRowVisibility(t *testing.T) { assert.NoError(t, err) assert.EqualError(t, f.SetRowVisible("Sheet3", 0, true), newInvalidRowNumberError(0).Error()) assert.EqualError(t, f.SetRowVisible("SheetN", 2, false), "sheet SheetN does not exist") + // Test set row visibility with invalid sheet name + assert.EqualError(t, f.SetRowVisible("Sheet:1", 1, false), ErrSheetNameInvalid.Error()) visible, err = f.GetRowVisible("Sheet3", 0) assert.Equal(t, false, visible) assert.EqualError(t, err, newInvalidRowNumberError(0).Error()) _, err = f.GetRowVisible("SheetN", 1) assert.EqualError(t, err, "sheet SheetN does not exist") - + // Test get row visibility with invalid sheet name + _, err = f.GetRowVisible("Sheet:1", 1) + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) } @@ -335,7 +344,9 @@ func TestRemoveRow(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) // Test remove row on not exist worksheet - assert.EqualError(t, f.RemoveRow("SheetN", 1), `sheet SheetN does not exist`) + assert.EqualError(t, f.RemoveRow("SheetN", 1), "sheet SheetN does not exist") + // Test remove row with invalid sheet name + assert.EqualError(t, f.RemoveRow("Sheet:1", 1), ErrSheetNameInvalid.Error()) } func TestInsertRows(t *testing.T) { @@ -365,6 +376,8 @@ func TestInsertRows(t *testing.T) { if !assert.Len(t, r.SheetData.Row, rowCount+4) { t.FailNow() } + // Test insert rows with invalid sheet name + assert.EqualError(t, f.InsertRows("Sheet:1", 1, 1), ErrSheetNameInvalid.Error()) assert.EqualError(t, f.InsertRows(sheet1, -1, 1), newInvalidRowNumberError(-1).Error()) assert.EqualError(t, f.InsertRows(sheet1, 0, 1), newInvalidRowNumberError(0).Error()) @@ -892,6 +905,12 @@ func TestDuplicateRowInvalidRowNum(t *testing.T) { } } +func TestDuplicateRow(t *testing.T) { + f := NewFile() + // Test duplicate row with invalid sheet name + assert.EqualError(t, f.DuplicateRowTo("Sheet:1", 1, 2), ErrSheetNameInvalid.Error()) +} + func TestDuplicateRowTo(t *testing.T) { f, sheetName := NewFile(), "Sheet1" // Test duplicate row with invalid target row number @@ -907,6 +926,8 @@ func TestDuplicateRowTo(t *testing.T) { assert.EqualError(t, f.DuplicateRowTo(sheetName, 1, 2), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // Test duplicate row on not exists worksheet assert.EqualError(t, f.DuplicateRowTo("SheetN", 1, 2), "sheet SheetN does not exist") + // Test duplicate row with invalid sheet name + assert.EqualError(t, f.DuplicateRowTo("Sheet:1", 1, 2), ErrSheetNameInvalid.Error()) } func TestDuplicateMergeCells(t *testing.T) { @@ -976,22 +997,24 @@ func TestSetRowStyle(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet1", "B2", "B2", style1)) assert.EqualError(t, f.SetRowStyle("Sheet1", 5, -1, style2), newInvalidRowNumberError(-1).Error()) assert.EqualError(t, f.SetRowStyle("Sheet1", 1, TotalRows+1, style2), ErrMaxRows.Error()) - // Test set row style with invalid style ID. + // Test set row style with invalid style ID assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, -1), newInvalidStyleID(-1).Error()) - // Test set row style with not exists style ID. + // Test set row style with not exists style ID assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, 10), newInvalidStyleID(10).Error()) assert.EqualError(t, f.SetRowStyle("SheetN", 1, 1, style2), "sheet SheetN does not exist") + // Test set row style with invalid sheet name + assert.EqualError(t, f.SetRowStyle("Sheet:1", 1, 1, 0), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SetRowStyle("Sheet1", 5, 1, style2)) cellStyleID, err := f.GetCellStyle("Sheet1", "B2") assert.NoError(t, err) assert.Equal(t, style2, cellStyleID) - // Test cell inheritance rows style. + // Test cell inheritance rows style assert.NoError(t, f.SetCellValue("Sheet1", "C1", nil)) cellStyleID, err = f.GetCellStyle("Sheet1", "C1") assert.NoError(t, err) assert.Equal(t, style2, cellStyleID) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRowStyle.xlsx"))) - // Test set row style with unsupported charset style sheet. + // Test set row style with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, cellStyleID), "XML syntax error on line 1: invalid UTF-8") diff --git a/shape_test.go b/shape_test.go index 2b2e87cb87..4c47d5870e 100644 --- a/shape_test.go +++ b/shape_test.go @@ -59,7 +59,7 @@ func TestAddShape(t *testing.T) { }`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) - // Test add first shape for given sheet. + // Test add first shape for given sheet f = NewFile() assert.NoError(t, f.AddShape("Sheet1", "A1", `{ "type": "ellipseRibbon", @@ -87,11 +87,13 @@ func TestAddShape(t *testing.T) { } }`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) - // Test add shape with unsupported charset style sheet. + // Test add shape with invalid sheet name + assert.EqualError(t, f.AddShape("Sheet:1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`), ErrSheetNameInvalid.Error()) + // Test add shape with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) assert.EqualError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`), "XML syntax error on line 1: invalid UTF-8") - // Test add shape with unsupported charset content types. + // Test add shape with unsupported charset content types f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) diff --git a/sheet.go b/sheet.go index bbf529ae82..16c4c16ca9 100644 --- a/sheet.go +++ b/sheet.go @@ -35,14 +35,15 @@ import ( // name and returns the index of the sheets in the workbook after it appended. // Note that when creating a new workbook, the default worksheet named // `Sheet1` will be created. -func (f *File) NewSheet(sheet string) int { - if trimSheetName(sheet) == "" { - return -1 +func (f *File) NewSheet(sheet string) (int, error) { + var err error + if err = checkSheetName(sheet); err != nil { + return -1, err } // Check if the worksheet already exists - index := f.GetSheetIndex(sheet) + index, err := f.GetSheetIndex(sheet) if index != -1 { - return index + return index, err } f.DeleteSheet(sheet) f.SheetCount++ @@ -235,7 +236,7 @@ func (f *File) setSheet(index int, name string) { }, } sheetXMLPath := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" - f.sheetMap[trimSheetName(name)] = sheetXMLPath + f.sheetMap[name] = sheetXMLPath f.Sheet.Store(sheetXMLPath, &ws) f.xmlAttr[sheetXMLPath] = []xml.Attr{NameSpaceSpreadSheet} } @@ -352,11 +353,16 @@ func (f *File) getActiveSheetID() int { // this function only changes the name of the sheet and will not update the // sheet name in the formula or reference associated with the cell. So there // may be problem formula error or reference missing. -func (f *File) SetSheetName(source, target string) { - source = trimSheetName(source) - target = trimSheetName(target) +func (f *File) SetSheetName(source, target string) error { + var err error + if err = checkSheetName(source); err != nil { + return err + } + if err = checkSheetName(target); err != nil { + return err + } if strings.EqualFold(target, source) { - return + return err } wb, _ := f.workbookReader() for k, v := range wb.Sheets.Sheet { @@ -366,6 +372,7 @@ func (f *File) SetSheetName(source, target string) { delete(f.sheetMap, source) } } + return err } // GetSheetName provides a function to get the sheet name of the workbook by @@ -385,9 +392,8 @@ func (f *File) GetSheetName(index int) (name string) { // given sheet name. If given worksheet name is invalid, will return an // integer type value -1. func (f *File) getSheetID(sheet string) int { - sheetName := trimSheetName(sheet) for sheetID, name := range f.GetSheetMap() { - if strings.EqualFold(name, sheetName) { + if strings.EqualFold(name, sheet) { return sheetID } } @@ -397,14 +403,16 @@ func (f *File) getSheetID(sheet string) int { // GetSheetIndex provides a function to get a sheet index of the workbook by // the given sheet name. If the given sheet name is invalid or sheet doesn't // exist, it will return an integer type value -1. -func (f *File) GetSheetIndex(sheet string) int { - sheetName := trimSheetName(sheet) +func (f *File) GetSheetIndex(sheet string) (int, error) { + if err := checkSheetName(sheet); err != nil { + return -1, err + } for index, name := range f.GetSheetList() { - if strings.EqualFold(name, sheetName) { - return index + if strings.EqualFold(name, sheet) { + return index, nil } } - return -1 + return -1, nil } // GetSheetMap provides a function to get worksheets, chart sheets, dialog @@ -474,7 +482,6 @@ func (f *File) getSheetXMLPath(sheet string) (string, bool) { name string ok bool ) - sheet = trimSheetName(sheet) for sheetName, filePath := range f.sheetMap { if strings.EqualFold(sheetName, sheet) { name, ok = filePath, true @@ -530,19 +537,22 @@ func (f *File) setSheetBackground(sheet, extension string, file []byte) error { // references such as formulas, charts, and so on. If there is any referenced // value of the deleted worksheet, it will cause a file error when you open // it. This function will be invalid when only one worksheet is left. -func (f *File) DeleteSheet(sheet string) { - if f.SheetCount == 1 || f.GetSheetIndex(sheet) == -1 { - return +func (f *File) DeleteSheet(sheet string) error { + if err := checkSheetName(sheet); err != nil { + return err + } + if idx, _ := f.GetSheetIndex(sheet); f.SheetCount == 1 || idx == -1 { + return nil } - sheetName := trimSheetName(sheet) + wb, _ := f.workbookReader() wbRels, _ := f.relsReader(f.getWorkbookRelsPath()) activeSheetName := f.GetSheetName(f.GetActiveSheetIndex()) - deleteLocalSheetID := f.GetSheetIndex(sheet) + deleteLocalSheetID, _ := f.GetSheetIndex(sheet) deleteAndAdjustDefinedNames(wb, deleteLocalSheetID) for idx, v := range wb.Sheets.Sheet { - if !strings.EqualFold(v.Name, sheetName) { + if !strings.EqualFold(v.Name, sheet) { continue } @@ -568,7 +578,9 @@ func (f *File) DeleteSheet(sheet string) { delete(f.xmlAttr, sheetXML) f.SheetCount-- } - f.SetActiveSheet(f.GetSheetIndex(activeSheetName)) + index, err := f.GetSheetIndex(activeSheetName) + f.SetActiveSheet(index) + return err } // deleteAndAdjustDefinedNames delete and adjust defined name in the workbook @@ -683,7 +695,9 @@ func (f *File) copySheet(from, to int) error { // // err := f.SetSheetVisible("Sheet1", false) func (f *File) SetSheetVisible(sheet string, visible bool) error { - sheet = trimSheetName(sheet) + if err := checkSheetName(sheet); err != nil { + return err + } wb, err := f.workbookReader() if err != nil { return err @@ -857,17 +871,20 @@ func (f *File) SetPanes(sheet, panes string) error { // name. For example, get visible state of Sheet1: // // f.GetSheetVisible("Sheet1") -func (f *File) GetSheetVisible(sheet string) bool { - name, visible := trimSheetName(sheet), false +func (f *File) GetSheetVisible(sheet string) (bool, error) { + var visible bool + if err := checkSheetName(sheet); err != nil { + return visible, err + } wb, _ := f.workbookReader() for k, v := range wb.Sheets.Sheet { - if strings.EqualFold(v.Name, name) { + if strings.EqualFold(v.Name, sheet) { if wb.Sheets.Sheet[k].State == "" || wb.Sheets.Sheet[k].State == "visible" { visible = true } } } - return visible + return visible, nil } // SearchSheet provides a function to get cell reference by given worksheet name, @@ -889,6 +906,9 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { regSearch bool result []string ) + if err := checkSheetName(sheet); err != nil { + return result, err + } for _, r := range reg { regSearch = r } @@ -897,7 +917,7 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { return result, ErrSheetNotExist{sheet} } if ws, ok := f.Sheet.Load(name); ok && ws != nil { - // flush data + // Flush data output, _ := xml.Marshal(ws.(*xlsxWorksheet)) f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } @@ -1051,7 +1071,7 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { // | // &F | Current workbook's file name // | -// &G | Drawing object as background +// &G | Drawing object as background (Not support currently) // | // &H | Shadow text format // | @@ -1167,47 +1187,47 @@ func (f *File) SetHeaderFooter(sheet string, settings *HeaderFooterOptions) erro // Password: "password", // EditScenarios: false, // }) -func (f *File) ProtectSheet(sheet string, settings *SheetProtectionOptions) error { +func (f *File) ProtectSheet(sheet string, opts *SheetProtectionOptions) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } - if settings == nil { - settings = &SheetProtectionOptions{ + if opts == nil { + opts = &SheetProtectionOptions{ EditObjects: true, EditScenarios: true, SelectLockedCells: true, } } ws.SheetProtection = &xlsxSheetProtection{ - AutoFilter: settings.AutoFilter, - DeleteColumns: settings.DeleteColumns, - DeleteRows: settings.DeleteRows, - FormatCells: settings.FormatCells, - FormatColumns: settings.FormatColumns, - FormatRows: settings.FormatRows, - InsertColumns: settings.InsertColumns, - InsertHyperlinks: settings.InsertHyperlinks, - InsertRows: settings.InsertRows, - Objects: settings.EditObjects, - PivotTables: settings.PivotTables, - Scenarios: settings.EditScenarios, - SelectLockedCells: settings.SelectLockedCells, - SelectUnlockedCells: settings.SelectUnlockedCells, + AutoFilter: opts.AutoFilter, + DeleteColumns: opts.DeleteColumns, + DeleteRows: opts.DeleteRows, + FormatCells: opts.FormatCells, + FormatColumns: opts.FormatColumns, + FormatRows: opts.FormatRows, + InsertColumns: opts.InsertColumns, + InsertHyperlinks: opts.InsertHyperlinks, + InsertRows: opts.InsertRows, + Objects: opts.EditObjects, + PivotTables: opts.PivotTables, + Scenarios: opts.EditScenarios, + SelectLockedCells: opts.SelectLockedCells, + SelectUnlockedCells: opts.SelectUnlockedCells, Sheet: true, - Sort: settings.Sort, + Sort: opts.Sort, } - if settings.Password != "" { - if settings.AlgorithmName == "" { - ws.SheetProtection.Password = genSheetPasswd(settings.Password) + if opts.Password != "" { + if opts.AlgorithmName == "" { + ws.SheetProtection.Password = genSheetPasswd(opts.Password) return err } - hashValue, saltValue, err := genISOPasswdHash(settings.Password, settings.AlgorithmName, "", int(sheetProtectionSpinCount)) + hashValue, saltValue, err := genISOPasswdHash(opts.Password, opts.AlgorithmName, "", int(sheetProtectionSpinCount)) if err != nil { return err } ws.SheetProtection.Password = "" - ws.SheetProtection.AlgorithmName = settings.AlgorithmName + ws.SheetProtection.AlgorithmName = opts.AlgorithmName ws.SheetProtection.SaltValue = saltValue ws.SheetProtection.HashValue = hashValue ws.SheetProtection.SpinCount = int(sheetProtectionSpinCount) @@ -1246,25 +1266,25 @@ func (f *File) UnprotectSheet(sheet string, password ...string) error { return err } -// trimSheetName provides a function to trim invalid characters by given worksheet -// name. -func trimSheetName(name string) string { - if strings.ContainsAny(name, ":\\/?*[]") || utf8.RuneCountInString(name) > 31 { - r := make([]rune, 0, 31) - for _, v := range name { - switch v { - case 58, 92, 47, 63, 42, 91, 93: // replace :\/?*[] - continue - default: - r = append(r, v) - } - if len(r) == 31 { - break - } - } - name = string(r) +// checkSheetName check whether there are illegal characters in the sheet name. +// 1. Confirm that the sheet name is not empty +// 2. Make sure to enter a name with no more than 31 characters +// 3. Make sure the first or last character of the name cannot be a single quote +// 4. Verify that the following characters are not included in the name :\/?*[] +func checkSheetName(name string) error { + if name == "" { + return ErrSheetNameBlank + } + if utf8.RuneCountInString(name) > MaxSheetNameLength { + return ErrSheetNameLength } - return name + if strings.HasPrefix(name, "'") || strings.HasSuffix(name, "'") { + return ErrSheetNameSingleQuote + } + if strings.ContainsAny(name, ":\\/?*[]") { + return ErrSheetNameInvalid + } + return nil } // SetPageLayout provides a function to sets worksheet page layout. @@ -1499,7 +1519,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { Data: definedName.RefersTo, } if definedName.Scope != "" { - if sheetIndex := f.GetSheetIndex(definedName.Scope); sheetIndex >= 0 { + if sheetIndex, _ := f.GetSheetIndex(definedName.Scope); sheetIndex >= 0 { d.LocalSheetID = &sheetIndex } } @@ -1579,7 +1599,7 @@ func (f *File) GetDefinedName() []DefinedName { // GroupSheets provides a function to group worksheets by given worksheets // name. Group worksheets must contain an active worksheet. func (f *File) GroupSheets(sheets []string) error { - // check an active worksheet in group worksheets + // Check an active worksheet in group worksheets var inActiveSheet bool activeSheet := f.GetActiveSheetIndex() sheetMap := f.GetSheetList() diff --git a/sheet_test.go b/sheet_test.go index 2494cfba1c..ed85c264a2 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -16,18 +16,27 @@ import ( func TestNewSheet(t *testing.T) { f := NewFile() f.NewSheet("Sheet2") - sheetID := f.NewSheet("sheet2") + sheetID, err := f.NewSheet("sheet2") + assert.NoError(t, err) f.SetActiveSheet(sheetID) - // delete original sheet - f.DeleteSheet(f.GetSheetName(f.GetSheetIndex("Sheet1"))) + // Test delete original sheet + idx, err := f.GetSheetIndex("Sheet1") + assert.NoError(t, err) + f.DeleteSheet(f.GetSheetName(idx)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewSheet.xlsx"))) - // create new worksheet with already exists name - assert.Equal(t, f.GetSheetIndex("Sheet2"), f.NewSheet("Sheet2")) - // create new worksheet with empty sheet name - assert.Equal(t, -1, f.NewSheet(":\\/?*[]")) + // Test create new worksheet with already exists name + sheetID, err = f.NewSheet("Sheet2") + assert.NoError(t, err) + idx, err = f.GetSheetIndex("Sheet2") + assert.NoError(t, err) + assert.Equal(t, idx, sheetID) + // Test create new worksheet with empty sheet name + sheetID, err = f.NewSheet(":\\/?*[]") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + assert.Equal(t, -1, sheetID) } -func TestSetPane(t *testing.T) { +func TestSetPanes(t *testing.T) { f := NewFile() assert.NoError(t, f.SetPanes("Sheet1", `{"freeze":false,"split":false}`)) f.NewSheet("Panes 2") @@ -38,6 +47,8 @@ func TestSetPane(t *testing.T) { assert.NoError(t, f.SetPanes("Panes 4", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`)) assert.EqualError(t, f.SetPanes("Panes 4", ""), "unexpected end of JSON input") assert.EqualError(t, f.SetPanes("SheetN", ""), "sheet SheetN does not exist") + // Test set panes with invalid sheet name + assert.EqualError(t, f.SetPanes("Sheet:1", `{"freeze":false,"split":false}`), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) // Test add pane on empty sheet views worksheet f = NewFile() @@ -52,11 +63,14 @@ func TestSearchSheet(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - // Test search in a not exists worksheet. + // Test search in a not exists worksheet _, err = f.SearchSheet("Sheet4", "") assert.EqualError(t, err, "sheet Sheet4 does not exist") + // Test search sheet with invalid sheet name + _, err = f.SearchSheet("Sheet:1", "") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) var expected []string - // Test search a not exists value. + // Test search a not exists value result, err := f.SearchSheet("Sheet1", "X") assert.NoError(t, err) assert.EqualValues(t, expected, result) @@ -120,23 +134,30 @@ func TestSetPageLayout(t *testing.T) { opts, err := f.GetPageLayout("Sheet1") assert.NoError(t, err) assert.Equal(t, expected, opts) - // Test set page layout on not exists worksheet. + // Test set page layout on not exists worksheet assert.EqualError(t, f.SetPageLayout("SheetN", nil), "sheet SheetN does not exist") + // Test set page layout with invalid sheet name + assert.EqualError(t, f.SetPageLayout("Sheet:1", nil), ErrSheetNameInvalid.Error()) } func TestGetPageLayout(t *testing.T) { f := NewFile() - // Test get page layout on not exists worksheet. + // Test get page layout on not exists worksheet _, err := f.GetPageLayout("SheetN") assert.EqualError(t, err, "sheet SheetN does not exist") + // Test get page layout with invalid sheet name + _, err = f.GetPageLayout("Sheet:1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) } func TestSetHeaderFooter(t *testing.T) { f := NewFile() assert.NoError(t, f.SetCellStr("Sheet1", "A1", "Test SetHeaderFooter")) - // Test set header and footer on not exists worksheet. + // Test set header and footer on not exists worksheet assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN does not exist") - // Test set header and footer with illegal setting. + // Test Sheet:1 with invalid sheet name + assert.EqualError(t, f.SetHeaderFooter("Sheet:1", nil), ErrSheetNameInvalid.Error()) + // Test set header and footer with illegal setting assert.EqualError(t, f.SetHeaderFooter("Sheet1", &HeaderFooterOptions{ OddHeader: strings.Repeat("c", MaxFieldLength+1), }), newFieldLengthError("OddHeader").Error()) @@ -190,13 +211,13 @@ func TestDefinedName(t *testing.T) { assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[0].RefersTo) assert.Exactly(t, 1, len(f.GetDefinedName())) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx"))) - // Test set defined name with unsupported charset workbook. + // Test set defined name with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.SetDefinedName(&DefinedName{ Name: "Amount", RefersTo: "Sheet1!$A$2:$D$5", }), "XML syntax error on line 1: invalid UTF-8") - // Test delete defined name with unsupported charset workbook. + // Test delete defined name with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.DeleteDefinedName(&DefinedName{Name: "Amount"}), @@ -211,6 +232,8 @@ func TestGroupSheets(t *testing.T) { } assert.EqualError(t, f.GroupSheets([]string{"Sheet1", "SheetN"}), "sheet SheetN does not exist") assert.EqualError(t, f.GroupSheets([]string{"Sheet2", "Sheet3"}), "group worksheet must contain an active worksheet") + // Test group sheets with invalid sheet name + assert.EqualError(t, f.GroupSheets([]string{"Sheet:1", "Sheet1"}), ErrSheetNameInvalid.Error()) assert.NoError(t, f.GroupSheets([]string{"Sheet1", "Sheet2"})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGroupSheets.xlsx"))) } @@ -232,6 +255,8 @@ func TestInsertPageBreak(t *testing.T) { assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) assert.EqualError(t, f.InsertPageBreak("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.InsertPageBreak("SheetN", "C3"), "sheet SheetN does not exist") + // Test insert page break with invalid sheet name + assert.EqualError(t, f.InsertPageBreak("Sheet:1", "C3"), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertPageBreak.xlsx"))) } @@ -258,6 +283,8 @@ func TestRemovePageBreak(t *testing.T) { assert.EqualError(t, f.RemovePageBreak("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.RemovePageBreak("SheetN", "C3"), "sheet SheetN does not exist") + // Test remove page break with invalid sheet name + assert.EqualError(t, f.RemovePageBreak("Sheet:1", "A3"), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemovePageBreak.xlsx"))) } @@ -305,7 +332,8 @@ func TestSetActiveSheet(t *testing.T) { f = NewFile() f.WorkBook.BookViews = nil - idx := f.NewSheet("Sheet2") + idx, err := f.NewSheet("Sheet2") + assert.NoError(t, err) ws, ok = f.Sheet.Load("xl/worksheets/sheet2.xml") assert.True(t, ok) ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}} @@ -314,9 +342,11 @@ func TestSetActiveSheet(t *testing.T) { func TestSetSheetName(t *testing.T) { f := NewFile() - // Test set worksheet with the same name. - f.SetSheetName("Sheet1", "Sheet1") + // Test set worksheet with the same name + assert.NoError(t, f.SetSheetName("Sheet1", "Sheet1")) assert.Equal(t, "Sheet1", f.GetSheetName(0)) + // Test set sheet name with invalid sheet name + assert.EqualError(t, f.SetSheetName("Sheet:1", "Sheet1"), ErrSheetNameInvalid.Error()) } func TestWorksheetWriter(t *testing.T) { @@ -348,7 +378,9 @@ func TestGetWorkbookRelsPath(t *testing.T) { func TestDeleteSheet(t *testing.T) { f := NewFile() - f.SetActiveSheet(f.NewSheet("Sheet2")) + idx, err := f.NewSheet("Sheet2") + assert.NoError(t, err) + f.SetActiveSheet(idx) f.NewSheet("Sheet3") f.DeleteSheet("Sheet1") assert.Equal(t, "Sheet2", f.GetSheetName(f.GetActiveSheetIndex())) @@ -363,8 +395,10 @@ func TestDeleteSheet(t *testing.T) { assert.NoError(t, f.AutoFilter("Sheet1", "A1", "A1", "")) assert.NoError(t, f.AutoFilter("Sheet2", "A1", "A1", "")) assert.NoError(t, f.AutoFilter("Sheet3", "A1", "A1", "")) - f.DeleteSheet("Sheet2") - f.DeleteSheet("Sheet1") + assert.NoError(t, f.DeleteSheet("Sheet2")) + assert.NoError(t, f.DeleteSheet("Sheet1")) + // Test delete sheet with invalid sheet name + assert.EqualError(t, f.DeleteSheet("Sheet:1"), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet2.xlsx"))) } @@ -382,14 +416,32 @@ func TestGetSheetID(t *testing.T) { func TestSetSheetVisible(t *testing.T) { f := NewFile() + // Test set sheet visible with invalid sheet name + assert.EqualError(t, f.SetSheetVisible("Sheet:1", false), ErrSheetNameInvalid.Error()) f.WorkBook.Sheets.Sheet[0].Name = "SheetN" assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "sheet SheetN does not exist") - // Test set sheet visible with unsupported charset workbook. + // Test set sheet visible with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "XML syntax error on line 1: invalid UTF-8") } +func TestGetSheetVisible(t *testing.T) { + f := NewFile() + // Test get sheet visible with invalid sheet name + visible, err := f.GetSheetVisible("Sheet:1") + assert.Equal(t, false, visible) + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) +} + +func TestGetSheetIndex(t *testing.T) { + f := NewFile() + // Test get sheet index with invalid sheet name + idx, err := f.GetSheetIndex("Sheet:1") + assert.Equal(t, -1, idx) + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) +} + func TestSetContentTypes(t *testing.T) { f := NewFile() // Test set content type with unsupported charset content types. @@ -482,8 +534,34 @@ func TestSetSheetBackgroundFromBytes(t *testing.T) { assert.NoError(t, img.Close()) assert.NoError(t, f.SetSheetBackgroundFromBytes(imageTypes, imageTypes, content)) } + // Test set worksheet background with invalid sheet name + img, err := os.Open(filepath.Join("test", "images", "excel.png")) + assert.NoError(t, err) + content, err := io.ReadAll(img) + assert.NoError(t, err) + assert.EqualError(t, f.SetSheetBackgroundFromBytes("Sheet:1", ".png", content), ErrSheetNameInvalid.Error()) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetBackgroundFromBytes.xlsx"))) assert.NoError(t, f.Close()) assert.EqualError(t, f.SetSheetBackgroundFromBytes("Sheet1", ".svg", nil), ErrParameterInvalid.Error()) } + +func TestCheckSheetName(t *testing.T) { + // Test valid sheet name + assert.NoError(t, checkSheetName("Sheet1")) + assert.NoError(t, checkSheetName("She'et1")) + // Test invalid sheet name, empty name + assert.EqualError(t, checkSheetName(""), ErrSheetNameBlank.Error()) + // Test invalid sheet name, include :\/?*[] + assert.EqualError(t, checkSheetName("Sheet:"), ErrSheetNameInvalid.Error()) + assert.EqualError(t, checkSheetName(`Sheet\`), ErrSheetNameInvalid.Error()) + assert.EqualError(t, checkSheetName("Sheet/"), ErrSheetNameInvalid.Error()) + assert.EqualError(t, checkSheetName("Sheet?"), ErrSheetNameInvalid.Error()) + assert.EqualError(t, checkSheetName("Sheet*"), ErrSheetNameInvalid.Error()) + assert.EqualError(t, checkSheetName("Sheet["), ErrSheetNameInvalid.Error()) + assert.EqualError(t, checkSheetName("Sheet]"), ErrSheetNameInvalid.Error()) + // Test invalid sheet name, single quotes at the front or at the end + assert.EqualError(t, checkSheetName("'Sheet"), ErrSheetNameSingleQuote.Error()) + assert.EqualError(t, checkSheetName("Sheet'"), ErrSheetNameSingleQuote.Error()) +} diff --git a/sheetpr_test.go b/sheetpr_test.go index d422e3f65b..daf6c191f6 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -27,15 +27,20 @@ func TestSetPageMargins(t *testing.T) { opts, err := f.GetPageMargins("Sheet1") assert.NoError(t, err) assert.Equal(t, expected, opts) - // Test set page margins on not exists worksheet. + // Test set page margins on not exists worksheet assert.EqualError(t, f.SetPageMargins("SheetN", nil), "sheet SheetN does not exist") + // Test set page margins with invalid sheet name + assert.EqualError(t, f.SetPageMargins("Sheet:1", nil), ErrSheetNameInvalid.Error()) } func TestGetPageMargins(t *testing.T) { f := NewFile() - // Test get page margins on not exists worksheet. + // Test get page margins on not exists worksheet _, err := f.GetPageMargins("SheetN") assert.EqualError(t, err, "sheet SheetN does not exist") + // Test get page margins with invalid sheet name + _, err = f.GetPageMargins("Sheet:1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) } func TestSetSheetProps(t *testing.T) { @@ -80,13 +85,18 @@ func TestSetSheetProps(t *testing.T) { ws.(*xlsxWorksheet).SheetPr = nil assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{TabColorTint: float64Ptr(1)})) - // Test SetSheetProps on not exists worksheet. + // Test set worksheet properties on not exists worksheet assert.EqualError(t, f.SetSheetProps("SheetN", nil), "sheet SheetN does not exist") + // Test set worksheet properties with invalid sheet name + assert.EqualError(t, f.SetSheetProps("Sheet:1", nil), ErrSheetNameInvalid.Error()) } func TestGetSheetProps(t *testing.T) { f := NewFile() - // Test GetSheetProps on not exists worksheet. + // Test get worksheet properties on not exists worksheet _, err := f.GetSheetProps("SheetN") assert.EqualError(t, err, "sheet SheetN does not exist") + // Test get worksheet properties with invalid sheet name + _, err = f.GetSheetProps("Sheet:1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) } diff --git a/sparkline_test.go b/sparkline_test.go index e20dfdc811..c2c1c41330 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -214,7 +214,7 @@ func TestAddSparkline(t *testing.T) { Negative: true, })) - // Save spreadsheet by the given path. + // Save spreadsheet by the given path assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddSparkline.xlsx"))) // Test error exceptions @@ -225,6 +225,14 @@ func TestAddSparkline(t *testing.T) { assert.EqualError(t, f.AddSparkline("Sheet1", nil), ErrParameterRequired.Error()) + // Test add sparkline with invalid sheet name + assert.EqualError(t, f.AddSparkline("Sheet:1", &SparklineOptions{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + Type: "win_loss", + Negative: true, + }), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Range: []string{"Sheet2!A3:E3"}, }), ErrSparklineLocation.Error()) diff --git a/stream.go b/stream.go index a575e761dd..0209e22701 100644 --- a/stream.go +++ b/stream.go @@ -102,6 +102,9 @@ type StreamWriter struct { // excelize.Cell{Value: 1}}, // excelize.RowOpts{StyleID: styleID, Height: 20, Hidden: false}); func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { + if err := checkSheetName(sheet); err != nil { + return nil, err + } sheetID := f.getSheetID(sheet) if sheetID == -1 { return nil, newNoExistSheetError(sheet) @@ -219,8 +222,8 @@ func (sw *StreamWriter) AddTable(hCell, vCell, opts string) error { sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml" tableXML := strings.ReplaceAll(sheetRelationshipsTableXML, "..", "xl") - // Add first table for given sheet. - sheetPath := sw.file.sheetMap[trimSheetName(sw.Sheet)] + // Add first table for given sheet + sheetPath := sw.file.sheetMap[sw.Sheet] sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" rID := sw.file.addRels(sheetRels, SourceRelationshipTable, sheetRelationshipsTableXML, "") @@ -661,7 +664,7 @@ func (sw *StreamWriter) Flush() error { return err } - sheetPath := sw.file.sheetMap[trimSheetName(sw.Sheet)] + sheetPath := sw.file.sheetMap[sw.Sheet] sw.file.Sheet.Delete(sheetPath) delete(sw.file.checked, sheetPath) sw.file.Pkg.Delete(sheetPath) diff --git a/stream_test.go b/stream_test.go index 41f54151c7..1a63e35fe3 100644 --- a/stream_test.go +++ b/stream_test.go @@ -257,6 +257,9 @@ func TestNewStreamWriter(t *testing.T) { assert.NoError(t, err) _, err = file.NewStreamWriter("SheetN") assert.EqualError(t, err, "sheet SheetN does not exist") + // Test new stream write with invalid sheet name + _, err = file.NewStreamWriter("Sheet:1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) } func TestStreamMarshalAttrs(t *testing.T) { diff --git a/styles.go b/styles.go index 79bd6d3622..0f0b560429 100644 --- a/styles.go +++ b/styles.go @@ -1101,11 +1101,25 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { return &fs, err } -// NewStyle provides a function to create the style for cells by given -// structure pointer or JSON. This function is concurrency safe. Note that the -// color field uses RGB color code. -// -// The following shows the border styles sorted by excelize index number: +// NewStyle provides a function to create the style for cells by given structure +// pointer or JSON. This function is concurrency safe. Note that +// the 'Font.Color' field uses an RGB color represented in 'RRGGBB' hexadecimal +// notation. +// +// The following table shows the border types used in 'Border.Type' supported by +// excelize: +// +// Type | Description +// --------------+------------------ +// left | Left border +// top | Top border +// right | Right border +// bottom | Bottom border +// diagonalDown | Diagonal down border +// diagonalUp | Diagonal up border +// +// The following table shows the border styles used in 'Border.Style' supported +// by excelize index number: // // Index | Name | Weight | Style // -------+---------------+--------+------------- @@ -1124,7 +1138,8 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { // 12 | Dash Dot Dot | 2 | - . . - . . // 13 | SlantDash Dot | 2 | / - . / - . // -// The following shows the borders in the order shown in the Excel dialog: +// The following table shows the border styles used in 'Border.Style' in the +// order shown in the Excel dialog: // // Index | Style | Index | Style // -------+-------------+-------+------------- @@ -1136,7 +1151,8 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { // 3 | - - - - - - | 5 | ----------- // 1 | ----------- | 6 | =========== // -// The following shows the shading styles sorted by excelize index number: +// The following table shows the shading styles used in 'Fill.Shading' supported +// by excelize index number: // // Index | Style | Index | Style // -------+-----------------+-------+----------------- @@ -1144,7 +1160,8 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { // 1 | Vertical | 4 | From corner // 2 | Diagonal Up | 5 | From center // -// The following shows the patterns styles sorted by excelize index number: +// The following table shows the pattern styles used in 'Fill.Pattern' supported +// by excelize index number: // // Index | Style | Index | Style // -------+-----------------+-------+----------------- @@ -1159,7 +1176,8 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { // 8 | darkUp | 18 | gray0625 // 9 | darkGrid | | // -// The following the type of horizontal alignment in cells: +// The following table shows the type of cells' horizontal alignment used +// in 'Alignment.Horizontal': // // Style // ------------------ @@ -1171,7 +1189,8 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { // centerContinuous // distributed // -// The following the type of vertical alignment in cells: +// The following table shows the type of cells' vertical alignment used in +// 'Alignment.Vertical': // // Style // ------------------ @@ -1180,7 +1199,8 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { // justify // distributed // -// The following the type of font underline style: +// The following table shows the type of font underline style used in +// 'Font.Underline': // // Style // ------------------ diff --git a/styles_test.go b/styles_test.go index 9001d5bef6..0d216b0b11 100644 --- a/styles_test.go +++ b/styles_test.go @@ -201,6 +201,9 @@ func TestGetConditionalFormats(t *testing.T) { f := NewFile() _, err := f.GetConditionalFormats("SheetN") assert.EqualError(t, err, "sheet SheetN does not exist") + // Test get conditional formats with invalid sheet name + _, err = f.GetConditionalFormats("Sheet:1") + assert.EqualError(t, err, ErrSheetNameInvalid.Error()) } func TestUnsetConditionalFormat(t *testing.T) { @@ -211,9 +214,11 @@ func TestUnsetConditionalFormat(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format))) assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10")) - // Test unset conditional format on not exists worksheet. + // Test unset conditional format on not exists worksheet assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN does not exist") - // Save spreadsheet by the given path. + // Test unset conditional format with invalid sheet name + assert.EqualError(t, f.UnsetConditionalFormat("Sheet:1", "A1:A10"), ErrSheetNameInvalid.Error()) + // Save spreadsheet by the given path assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx"))) } @@ -240,7 +245,7 @@ func TestNewStyle(t *testing.T) { _, err = f.NewStyle(&Style{Font: &Font{Size: MaxFontSize + 1}}) assert.EqualError(t, err, ErrFontSize.Error()) - // Test create numeric custom style. + // Test create numeric custom style numFmt := "####;####" f.Styles.NumFmts = nil styleID, err = f.NewStyle(&Style{ @@ -256,7 +261,7 @@ func TestNewStyle(t *testing.T) { nf := f.Styles.CellXfs.Xf[styleID] assert.Equal(t, 164, *nf.NumFmtID) - // Test create currency custom style. + // Test create currency custom style f.Styles.NumFmts = nil styleID, err = f.NewStyle(&Style{ Lang: "ko-kr", @@ -273,7 +278,7 @@ func TestNewStyle(t *testing.T) { nf = f.Styles.CellXfs.Xf[styleID] assert.Equal(t, 32, *nf.NumFmtID) - // Test set build-in scientific number format. + // Test set build-in scientific number format styleID, err = f.NewStyle(&Style{NumFmt: 11}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "B1", styleID)) @@ -283,7 +288,7 @@ func TestNewStyle(t *testing.T) { assert.Equal(t, [][]string{{"1.23E+00", "1.23E+00"}}, rows) f = NewFile() - // Test currency number format. + // Test currency number format customNumFmt := "[$$-409]#,##0.00" style1, err := f.NewStyle(&Style{CustomNumFmt: &customNumFmt}) assert.NoError(t, err) @@ -309,7 +314,7 @@ func TestNewStyle(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 0, style5) - // Test create style with unsupported charset style sheet. + // Test create style with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) _, err = f.NewStyle(&Style{NumFmt: 165}) @@ -318,7 +323,7 @@ func TestNewStyle(t *testing.T) { func TestNewConditionalStyle(t *testing.T) { f := NewFile() - // Test create conditional style with unsupported charset style sheet. + // Test create conditional style with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) _, err := f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) @@ -330,7 +335,7 @@ func TestGetDefaultFont(t *testing.T) { s, err := f.GetDefaultFont() assert.NoError(t, err) assert.Equal(t, s, "Calibri", "Default font should be Calibri") - // Test get default font with unsupported charset style sheet. + // Test get default font with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) _, err = f.GetDefaultFont() @@ -346,7 +351,7 @@ func TestSetDefaultFont(t *testing.T) { assert.NoError(t, err) assert.Equal(t, s, "Arial", "Default font should change to Arial") assert.Equal(t, *styles.CellStyles.CellStyle[0].CustomBuiltIn, true) - // Test set default font with unsupported charset style sheet. + // Test set default font with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) assert.EqualError(t, f.SetDefaultFont("Arial"), "XML syntax error on line 1: invalid UTF-8") @@ -354,7 +359,7 @@ func TestSetDefaultFont(t *testing.T) { func TestStylesReader(t *testing.T) { f := NewFile() - // Test read styles with unsupported charset. + // Test read styles with unsupported charset f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) styles, err := f.stylesReader() @@ -364,7 +369,7 @@ func TestStylesReader(t *testing.T) { func TestThemeReader(t *testing.T) { f := NewFile() - // Test read theme with unsupported charset. + // Test read theme with unsupported charset f.Pkg.Store(defaultXMLPathTheme, MacintoshCyrillicCharset) theme, err := f.themeReader() assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") diff --git a/table.go b/table.go index 06336ff337..90cc97fd5a 100644 --- a/table.go +++ b/table.go @@ -304,7 +304,10 @@ func (f *File) AutoFilter(sheet, hCell, vCell, opts string) error { if err != nil { return err } - sheetID := f.GetSheetIndex(sheet) + sheetID, err := f.GetSheetIndex(sheet) + if err != nil { + return err + } filterRange := fmt.Sprintf("'%s'!%s", sheet, ref) d := xlsxDefinedName{ Name: filterDB, diff --git a/table_test.go b/table_test.go index 5ac464b19e..d26d20ca9a 100644 --- a/table_test.go +++ b/table_test.go @@ -10,36 +10,24 @@ import ( func TestAddTable(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - err = f.AddTable("Sheet1", "B26", "A21", `{}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - err = f.AddTable("Sheet2", "A2", "B5", `{"table_name":"table","table_style":"TableStyleMedium2", "show_first_column":true,"show_last_column":true,"show_row_stripes":false,"show_column_stripes":true}`) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) + assert.NoError(t, f.AddTable("Sheet1", "B26", "A21", `{}`)) + assert.NoError(t, f.AddTable("Sheet2", "A2", "B5", `{"table_name":"table","table_style":"TableStyleMedium2", "show_first_column":true,"show_last_column":true,"show_row_stripes":false,"show_column_stripes":true}`)) + assert.NoError(t, f.AddTable("Sheet2", "F1", "F1", `{"table_style":"TableStyleMedium8"}`)) - err = f.AddTable("Sheet2", "F1", "F1", `{"table_style":"TableStyleMedium8"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - // Test add table in not exist worksheet. + // Test add table in not exist worksheet assert.EqualError(t, f.AddTable("SheetN", "B26", "A21", `{}`), "sheet SheetN does not exist") - // Test add table with illegal options. + // Test add table with illegal options assert.EqualError(t, f.AddTable("Sheet1", "B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") - // Test add table with illegal cell reference. + // Test add table with illegal cell reference assert.EqualError(t, f.AddTable("Sheet1", "A", "B1", `{}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.AddTable("Sheet1", "A1", "B", `{}`), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) - // Test addTable with illegal cell reference. + // Test add table with invalid sheet name + assert.EqualError(t, f.AddTable("Sheet:1", "B26", "A21", `{}`), ErrSheetNameInvalid.Error()) + // Test addTable with illegal cell reference f = NewFile() assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell reference [0, 0]") assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell reference [0, 0]") @@ -53,12 +41,8 @@ func TestSetTableHeader(t *testing.T) { func TestAutoFilter(t *testing.T) { outFile := filepath.Join("test", "TestAutoFilter%d.xlsx") - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - + assert.NoError(t, err) formats := []string{ ``, `{"column":"B","expression":"x != blanks"}`, @@ -69,7 +53,6 @@ func TestAutoFilter(t *testing.T) { `{"column":"B","expression":"x == 1 or x == 2"}`, `{"column":"B","expression":"x == 1 or x == 2*"}`, } - for i, format := range formats { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { err = f.AutoFilter("Sheet1", "D4", "B1", format) @@ -78,10 +61,12 @@ func TestAutoFilter(t *testing.T) { }) } - // Test add auto filter with illegal cell reference. + // Test add auto filter with invalid sheet name + assert.EqualError(t, f.AutoFilter("Sheet:1", "A1", "B1", ""), ErrSheetNameInvalid.Error()) + // Test add auto filter with illegal cell reference assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) - // Test add auto filter with unsupported charset workbook. + // Test add auto filter with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.AutoFilter("Sheet1", "D4", "B1", formats[0]), "XML syntax error on line 1: invalid UTF-8") @@ -132,10 +117,10 @@ func TestAutoFilterError(t *testing.T) { func TestParseFilterTokens(t *testing.T) { f := NewFile() - // Test with unknown operator. + // Test with unknown operator _, _, err := f.parseFilterTokens("", []string{"", "!"}) assert.EqualError(t, err, "unknown operator: !") - // Test invalid operator in context. + // Test invalid operator in context _, _, err = f.parseFilterTokens("", []string{"", "<", "x != blanks"}) assert.EqualError(t, err, "the operator '<' in expression '' is not valid in relation to Blanks/NonBlanks'") } diff --git a/workbook.go b/workbook.go index eb57cb5628..4974f75b61 100644 --- a/workbook.go +++ b/workbook.go @@ -64,7 +64,7 @@ func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error) { func (f *File) setWorkbook(name string, sheetID, rid int) { wb, _ := f.workbookReader() wb.Sheets.Sheet = append(wb.Sheets.Sheet, xlsxSheet{ - Name: trimSheetName(name), + Name: name, SheetID: sheetID, ID: "rId" + strconv.Itoa(rid), }) diff --git a/xmlComments.go b/xmlComments.go index 7b67e67823..13b727b780 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -77,6 +77,6 @@ type Comment struct { Author string `json:"author"` AuthorID int `json:"author_id"` Cell string `json:"cell"` - Text string `json:"string"` + Text string `json:"text"` Runs []RichTextRun `json:"runs"` } diff --git a/xmlDrawing.go b/xmlDrawing.go index 56ddc0e780..9af6905e34 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -19,58 +19,30 @@ import ( // Source relationship and namespace list, associated prefixes and schema in which it was // introduced. var ( - SourceRelationship = xml.Attr{Name: xml.Name{Local: "r", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"} - SourceRelationshipCompatibility = xml.Attr{Name: xml.Name{Local: "mc", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/markup-compatibility/2006"} - SourceRelationshipChart20070802 = xml.Attr{Name: xml.Name{Local: "c14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"} - SourceRelationshipChart2014 = xml.Attr{Name: xml.Name{Local: "c16", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2014/chart"} - SourceRelationshipChart201506 = xml.Attr{Name: xml.Name{Local: "c16r2", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2015/06/chart"} - NameSpaceSpreadSheet = xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"} - NameSpaceSpreadSheetX14 = xml.Attr{Name: xml.Name{Local: "x14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"} + NameSpaceDocumentPropertiesVariantTypes = xml.Attr{Name: xml.Name{Local: "vt", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"} + NameSpaceDrawing2016SVG = xml.Attr{Name: xml.Name{Local: "asvg", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main"} NameSpaceDrawingML = xml.Attr{Name: xml.Name{Local: "a", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/main"} NameSpaceDrawingMLChart = xml.Attr{Name: xml.Name{Local: "c", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/chart"} NameSpaceDrawingMLSpreadSheet = xml.Attr{Name: xml.Name{Local: "xdr", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"} - NameSpaceDrawing2016SVG = xml.Attr{Name: xml.Name{Local: "asvg", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main"} - NameSpaceSpreadSheetX15 = xml.Attr{Name: xml.Name{Local: "x15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"} - NameSpaceSpreadSheetExcel2006Main = xml.Attr{Name: xml.Name{Local: "xne", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/excel/2006/main"} NameSpaceMacExcel2008Main = xml.Attr{Name: xml.Name{Local: "mx", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/mac/excel/2008/main"} - NameSpaceDocumentPropertiesVariantTypes = xml.Attr{Name: xml.Name{Local: "vt", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"} + NameSpaceSpreadSheet = xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"} + NameSpaceSpreadSheetExcel2006Main = xml.Attr{Name: xml.Name{Local: "xne", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/excel/2006/main"} + NameSpaceSpreadSheetX14 = xml.Attr{Name: xml.Name{Local: "x14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"} + NameSpaceSpreadSheetX15 = xml.Attr{Name: xml.Name{Local: "x15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"} + SourceRelationship = xml.Attr{Name: xml.Name{Local: "r", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"} + SourceRelationshipChart20070802 = xml.Attr{Name: xml.Name{Local: "c14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"} + SourceRelationshipChart2014 = xml.Attr{Name: xml.Name{Local: "c16", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2014/chart"} + SourceRelationshipChart201506 = xml.Attr{Name: xml.Name{Local: "c16r2", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2015/06/chart"} + SourceRelationshipCompatibility = xml.Attr{Name: xml.Name{Local: "mc", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/markup-compatibility/2006"} ) // Source relationship and namespace. const ( - SourceRelationshipOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" - SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" - SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" - SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" - SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" - SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" - SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" - SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" - SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" - SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" - SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet" - SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" - SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" - SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" - SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" - NameSpaceXML = "http://www.w3.org/XML/1998/namespace" - NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" - StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" - StrictSourceRelationshipOfficeDocument = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument" - StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart" - StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments" - StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image" - StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" - NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" - NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" - NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/" + ContentTypeAddinMacro = "application/vnd.ms-excel.addin.macroEnabled.main+xml" ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml" ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" - ContentTypeSheetML = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" - ContentTypeTemplate = "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" - ContentTypeAddinMacro = "application/vnd.ms-excel.addin.macroEnabled.main+xml" ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml" - ContentTypeTemplateMacro = "application/vnd.ms-excel.template.macroEnabled.main+xml" + ContentTypeSheetML = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" ContentTypeSpreadSheetMLChartsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" @@ -78,44 +50,73 @@ const ( ContentTypeSpreadSheetMLSharedStrings = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml" ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" + ContentTypeTemplate = "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" + ContentTypeTemplateMacro = "application/vnd.ms-excel.template.macroEnabled.main+xml" ContentTypeVBA = "application/vnd.ms-office.vbaProject" ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing" + NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" + NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/" + NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" + NameSpaceXML = "http://www.w3.org/XML/1998/namespace" + NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" + SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" + SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" + SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" + SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet" + SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" + SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" + SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" + SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" + SourceRelationshipOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" + SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" + SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" + SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" + SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" + SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" + SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" + StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" + StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" + StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart" + StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments" + StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image" + StrictSourceRelationshipOfficeDocument = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument" // ExtURIConditionalFormattings is the extLst child element // ([ISO/IEC29500-1:2016] section 18.2.10) of the worksheet element // ([ISO/IEC29500-1:2016] section 18.3.1.99) is extended by the addition of // new child ext elements ([ISO/IEC29500-1:2016] section 18.2.7) ExtURIConditionalFormattings = "{78C0D931-6437-407D-A8EE-F0AAD7539E65}" ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}" - ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" - ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" - ExtURISlicerCachesListX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}" - ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}" - ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}" - ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}" - ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}" - ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}" ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}" + ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}" ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}" + ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}" + ExtURISlicerCachesListX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}" + ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" + ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}" + ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}" + ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}" + ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}" ) // Excel specifications and limits const ( - UnzipSizeLimit = 1000 << 24 - StreamChunkSize = 1 << 24 + MaxCellStyles = 64000 + MaxColumns = 16384 + MaxColumnWidth = 255 + MaxFieldLength = 255 + MaxFilePathLength = 207 MaxFontFamilyLength = 31 MaxFontSize = 409 - MaxFilePathLength = 207 - MaxFieldLength = 255 - MaxColumnWidth = 255 MaxRowHeight = 409 - MaxCellStyles = 64000 + MaxSheetNameLength = 31 + MinColumns = 1 MinFontSize = 1 + StreamChunkSize = 1 << 24 + TotalCellChars = 32767 TotalRows = 1048576 - MinColumns = 1 - MaxColumns = 16384 TotalSheetHyperlinks = 65529 - TotalCellChars = 32767 + UnzipSizeLimit = 1000 << 24 // pivotTableVersion should be greater than 3. One or more of the // PivotTables chosen are created in a version of Excel earlier than // Excel 2007 or in compatibility mode. Slicer can only be used with From 0c76766c2b192b816db0d8196c19e8c0506e725c Mon Sep 17 00:00:00 2001 From: Gin Date: Tue, 27 Dec 2022 00:06:18 +0800 Subject: [PATCH 139/213] Add support for workbook protection (#1431) --- crypt.go | 21 +++++++++-------- errors.go | 6 +++++ excelize_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++ workbook.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ xmlWorkbook.go | 8 +++++++ 5 files changed, 141 insertions(+), 10 deletions(-) diff --git a/crypt.go b/crypt.go index 5dd8b0c122..dc8e35f652 100644 --- a/crypt.go +++ b/crypt.go @@ -37,16 +37,17 @@ import ( ) var ( - blockKey = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption - oleIdentifier = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1} - headerCLSID = make([]byte, 16) - difSect = -4 - endOfChain = -2 - fatSect = -3 - iterCount = 50000 - packageEncryptionChunkSize = 4096 - packageOffset = 8 // First 8 bytes are the size of the stream - sheetProtectionSpinCount = 1e5 + blockKey = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption + oleIdentifier = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1} + headerCLSID = make([]byte, 16) + difSect = -4 + endOfChain = -2 + fatSect = -3 + iterCount = 50000 + packageEncryptionChunkSize = 4096 + packageOffset = 8 // First 8 bytes are the size of the stream + sheetProtectionSpinCount = 1e5 + workbookProtectionSpinCount = 1e5 ) // Encryption specifies the encryption structure, streams, and storages are diff --git a/errors.go b/errors.go index 7a31a4cd0a..d6e0b4198e 100644 --- a/errors.go +++ b/errors.go @@ -230,4 +230,10 @@ var ( // ErrSheetNameLength defined the error message on receiving the sheet // name length exceeds the limit. ErrSheetNameLength = fmt.Errorf("the sheet name length exceeds the %d characters limit", MaxSheetNameLength) + // ErrUnprotectWorkbook defined the error message on workbook has set no + // protection. + ErrUnprotectWorkbook = errors.New("workbook has set no protect") + // ErrUnprotectWorkbookPassword defined the error message on remove workbook + // protection with password verification failed. + ErrUnprotectWorkbookPassword = errors.New("workbook protect password not match") ) diff --git a/excelize_test.go b/excelize_test.go index 1dad0ff541..673664c7cb 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1329,6 +1329,61 @@ func TestUnprotectSheet(t *testing.T) { assert.EqualError(t, f.UnprotectSheet(sheetName, "wrongPassword"), "illegal base64 data at input byte 8") } +func TestProtectWorkbook(t *testing.T) { + f := NewFile() + assert.NoError(t, f.ProtectWorkbook(nil)) + // Test protect workbook with default hash algorithm + assert.NoError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{ + Password: "password", + LockStructure: true, + })) + wb, err := f.workbookReader() + assert.NoError(t, err) + assert.Equal(t, "SHA-512", wb.WorkbookProtection.WorkbookAlgorithmName) + assert.Equal(t, 24, len(wb.WorkbookProtection.WorkbookSaltValue)) + assert.Equal(t, 88, len(wb.WorkbookProtection.WorkbookHashValue)) + assert.Equal(t, int(workbookProtectionSpinCount), wb.WorkbookProtection.WorkbookSpinCount) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestProtectWorkbook.xlsx"))) + // Test protect workbook with password exceeds the limit length + assert.EqualError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{ + AlgorithmName: "MD4", + Password: strings.Repeat("s", MaxFieldLength+1), + }), ErrPasswordLengthInvalid.Error()) + // Test protect workbook with unsupported hash algorithm + assert.EqualError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{ + AlgorithmName: "RIPEMD-160", + Password: "password", + }), ErrUnsupportedHashAlgorithm.Error()) +} + +func TestUnprotectWorkbook(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + assert.NoError(t, f.UnprotectWorkbook()) + assert.EqualError(t, f.UnprotectWorkbook("password"), ErrUnprotectWorkbook.Error()) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnprotectWorkbook.xlsx"))) + assert.NoError(t, f.Close()) + + f = NewFile() + assert.NoError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{Password: "password"})) + // Test remove workbook protection with an incorrect password + assert.EqualError(t, f.UnprotectWorkbook("wrongPassword"), ErrUnprotectWorkbookPassword.Error()) + // Test remove workbook protection with password verification + assert.NoError(t, f.UnprotectWorkbook("password")) + // Test with invalid salt value + assert.NoError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{ + AlgorithmName: "SHA-512", + Password: "password", + })) + wb, err := f.workbookReader() + assert.NoError(t, err) + wb.WorkbookProtection.WorkbookSaltValue = "YWJjZA=====" + assert.EqualError(t, f.UnprotectWorkbook("wrongPassword"), "illegal base64 data at input byte 8") +} + func TestSetDefaultTimeStyle(t *testing.T) { f := NewFile() // Test set default time style on not exists worksheet. diff --git a/workbook.go b/workbook.go index 4974f75b61..1367eac82f 100644 --- a/workbook.go +++ b/workbook.go @@ -59,6 +59,67 @@ func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error) { return opts, err } +// ProtectWorkbook provides a function to prevent other users from accidentally or +// deliberately changing, moving, or deleting data in a workbook. +func (f *File) ProtectWorkbook(opts *WorkbookProtectionOptions) error { + wb, err := f.workbookReader() + if err != nil { + return err + } + if wb.WorkbookProtection == nil { + wb.WorkbookProtection = new(xlsxWorkbookProtection) + } + if opts == nil { + opts = &WorkbookProtectionOptions{} + } + wb.WorkbookProtection = &xlsxWorkbookProtection{ + LockStructure: opts.LockStructure, + LockWindows: opts.LockWindows, + } + if opts.Password != "" { + if opts.AlgorithmName == "" { + opts.AlgorithmName = "SHA-512" + } + hashValue, saltValue, err := genISOPasswdHash(opts.Password, opts.AlgorithmName, "", int(workbookProtectionSpinCount)) + if err != nil { + return err + } + wb.WorkbookProtection.WorkbookAlgorithmName = opts.AlgorithmName + wb.WorkbookProtection.WorkbookSaltValue = saltValue + wb.WorkbookProtection.WorkbookHashValue = hashValue + wb.WorkbookProtection.WorkbookSpinCount = int(workbookProtectionSpinCount) + } + return nil +} + +// UnprotectWorkbook provides a function to remove protection for workbook, +// specified the second optional password parameter to remove workbook +// protection with password verification. +func (f *File) UnprotectWorkbook(password ...string) error { + wb, err := f.workbookReader() + if err != nil { + return err + } + // password verification + if len(password) > 0 { + if wb.WorkbookProtection == nil { + return ErrUnprotectWorkbook + } + if wb.WorkbookProtection.WorkbookAlgorithmName != "" { + // check with given salt value + hashValue, _, err := genISOPasswdHash(password[0], wb.WorkbookProtection.WorkbookAlgorithmName, wb.WorkbookProtection.WorkbookSaltValue, wb.WorkbookProtection.WorkbookSpinCount) + if err != nil { + return err + } + if wb.WorkbookProtection.WorkbookHashValue != hashValue { + return ErrUnprotectWorkbookPassword + } + } + } + wb.WorkbookProtection = nil + return err +} + // setWorkbook update workbook property of the spreadsheet. Maximum 31 // characters are allowed in sheet title. func (f *File) setWorkbook(name string, sheetID, rid int) { diff --git a/xmlWorkbook.go b/xmlWorkbook.go index e384807c7a..503eac160e 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -320,3 +320,11 @@ type WorkbookPropsOptions struct { FilterPrivacy *bool `json:"filter_privacy,omitempty"` CodeName *string `json:"code_name,omitempty"` } + +// WorkbookProtectionOptions directly maps the settings of workbook protection. +type WorkbookProtectionOptions struct { + AlgorithmName string `json:"algorithmName,omitempty"` + Password string `json:"password,omitempty"` + LockStructure bool `json:"lockStructure,omitempty"` + LockWindows bool `json:"lockWindows,omitempty"` +} From a57203a03a54ded9c8e60ac149f95cecd4b51326 Mon Sep 17 00:00:00 2001 From: Liron Levin Date: Wed, 28 Dec 2022 18:37:37 +0200 Subject: [PATCH 140/213] This closes #1432, fix panic formattedValue when style is negative (#1433) --- cell.go | 2 +- cell_test.go | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cell.go b/cell.go index bbbb83a9ed..fe393ec74f 100644 --- a/cell.go +++ b/cell.go @@ -1319,7 +1319,7 @@ func (f *File) formattedValue(s int, v string, raw bool) (string, error) { if styleSheet.CellXfs == nil { return v, err } - if s >= len(styleSheet.CellXfs.Xf) { + if s >= len(styleSheet.CellXfs.Xf) || s < 0 { return v, err } var numFmtID int diff --git a/cell_test.go b/cell_test.go index 69a3f81d32..2a9357a774 100644 --- a/cell_test.go +++ b/cell_test.go @@ -2,6 +2,7 @@ package excelize import ( "fmt" + _ "image/jpeg" "os" "path/filepath" "reflect" @@ -11,8 +12,6 @@ import ( "testing" "time" - _ "image/jpeg" - "github.com/stretchr/testify/assert" ) @@ -755,10 +754,16 @@ func TestFormattedValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "43528", result) + // S is too large result, err = f.formattedValue(15, "43528", false) assert.NoError(t, err) assert.Equal(t, "43528", result) + // S is too small + result, err = f.formattedValue(-15, "43528", false) + assert.NoError(t, err) + assert.Equal(t, "43528", result) + result, err = f.formattedValue(1, "43528", false) assert.NoError(t, err) assert.Equal(t, "43528", result) From f58dabd4923ec817521657074c688a9d972b0576 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 30 Dec 2022 00:50:08 +0800 Subject: [PATCH 141/213] Breaking change: changed the function signature for 11 exported functions * Change `func (f *File) NewConditionalStyle(style string) (int, error)` to `func (f *File) NewConditionalStyle(style *Style) (int, error)` * Change `func (f *File) NewStyle(style interface{}) (int, error)` to `func (f *File) NewStyle(style *Style) (int, error)` * Change `func (f *File) AddChart(sheet, cell, opts string, combo ...string) error` to `func (f *File) AddChart(sheet, cell string, chart *ChartOptions, combo ...*ChartOptions) error` * Change `func (f *File) AddChartSheet(sheet, opts string, combo ...string) error` to `func (f *File) AddChartSheet(sheet string, chart *ChartOptions, combo ...*ChartOptions) error` * Change `func (f *File) AddShape(sheet, cell, opts string) error` to `func (f *File) AddShape(sheet, cell string, opts *Shape) error` * Change `func (f *File) AddPictureFromBytes(sheet, cell, opts, name, extension string, file []byte) error` to `func (f *File) AddPictureFromBytes(sheet, cell, name, extension string, file []byte, opts *PictureOptions) error` * Change `func (f *File) AddTable(sheet, hCell, vCell, opts string) error` to `func (f *File) AddTable(sheet, reference string, opts *TableOptions) error` * Change `func (sw *StreamWriter) AddTable(hCell, vCell, opts string) error` to `func (sw *StreamWriter) AddTable(reference string, opts *TableOptions) error` * Change `func (f *File) AutoFilter(sheet, hCell, vCell, opts string) error` to `func (f *File) AutoFilter(sheet, reference string, opts *AutoFilterOptions) error` * Change `func (f *File) SetPanes(sheet, panes string) error` to `func (f *File) SetPanes(sheet string, panes *Panes) error` * Change `func (sw *StreamWriter) AddTable(hCell, vCell, opts string) error` to `func (sw *StreamWriter) AddTable(reference string, opts *TableOptions) error` * Change `func (f *File) SetConditionalFormat(sheet, reference, opts string) error` to `func (f *File) SetConditionalFormat(sheet, reference string, opts []ConditionalFormatOptions) error` * Add exported types: * AutoFilterListOptions * AutoFilterOptions * Chart * ChartAxis * ChartDimension * ChartLegend * ChartLine * ChartMarker * ChartPlotArea * ChartSeries * ChartTitle * ConditionalFormatOptions * PaneOptions * Panes * PictureOptions * Shape * ShapeColor * ShapeLine * ShapeParagraph * TableOptions * This added support for set sheet visible as very hidden * Return error when missing required parameters for set defined name * Update unit test and comments --- README.md | 83 +++--- README_zh.md | 83 +++--- adjust_test.go | 43 +-- calc.go | 4 +- calc_test.go | 6 +- calcchain_test.go | 6 +- cell.go | 15 +- cell_test.go | 15 +- chart.go | 463 ++++++++++++++++--------------- chart_test.go | 318 +++++++++++++--------- col_test.go | 12 +- datavalidation_test.go | 3 +- docProps.go | 7 + docProps_test.go | 4 +- drawing.go | 112 ++++---- drawing_test.go | 6 +- excelize_test.go | 600 ++++++++++++++++++++++------------------- lib.go | 15 +- merge_test.go | 3 +- picture.go | 109 +++++--- picture_test.go | 69 ++--- pivotTable.go | 52 ++-- pivotTable_test.go | 5 +- rows_test.go | 16 +- shape.go | 104 +++---- shape_test.go | 135 +++++----- sheet.go | 203 ++++++++------ sheet_test.go | 140 +++++++--- sheetview_test.go | 8 +- sparkline_test.go | 17 +- stream.go | 37 ++- stream_test.go | 73 ++--- styles.go | 430 +++++++++++++++++++---------- styles_test.go | 125 +++++---- table.go | 122 ++++----- table_test.go | 91 ++++--- workbook.go | 18 +- workbook_test.go | 4 +- xmlApp.go | 14 +- xmlChart.go | 211 ++++++--------- xmlComments.go | 10 +- xmlDrawing.go | 87 +++--- xmlSharedStrings.go | 4 +- xmlStyles.go | 78 +++--- xmlTable.go | 39 +-- xmlWorkbook.go | 22 +- xmlWorksheet.go | 276 +++++++++---------- 47 files changed, 2317 insertions(+), 1980 deletions(-) diff --git a/README.md b/README.md index 008efc51b5..b261206a5b 100644 --- a/README.md +++ b/README.md @@ -121,41 +121,40 @@ import ( ) func main() { - categories := map[string]string{ - "A2": "Small", "A3": "Normal", "A4": "Large", - "B1": "Apple", "C1": "Orange", "D1": "Pear"} - values := map[string]int{ - "B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8} f := excelize.NewFile() - for k, v := range categories { - f.SetCellValue("Sheet1", k, v) - } - for k, v := range values { - f.SetCellValue("Sheet1", k, v) + for idx, row := range [][]interface{}{ + {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3}, + {"Normal", 5, 2, 4}, {"Large", 6, 7, 8}, + } { + cell, err := excelize.CoordinatesToCellName(1, idx+1) + if err != nil { + fmt.Println(err) + return + } + f.SetSheetRow("Sheet1", cell, &row) } - if err := f.AddChart("Sheet1", "E1", `{ - "type": "col3DClustered", - "series": [ - { - "name": "Sheet1!$A$2", - "categories": "Sheet1!$B$1:$D$1", - "values": "Sheet1!$B$2:$D$2" + if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ + Type: "col3DClustered", + Series: []excelize.ChartSeries{ + { + Name: "Sheet1!$A$2", + Categories: "Sheet1!$B$1:$D$1", + Values: "Sheet1!$B$2:$D$2", + }, + { + Name: "Sheet1!$A$3", + Categories: "Sheet1!$B$1:$D$1", + Values: "Sheet1!$B$3:$D$3", + }, + { + Name: "Sheet1!$A$4", + Categories: "Sheet1!$B$1:$D$1", + Values: "Sheet1!$B$4:$D$4", + }}, + Title: excelize.ChartTitle{ + Name: "Fruit 3D Clustered Column Chart", }, - { - "name": "Sheet1!$A$3", - "categories": "Sheet1!$B$1:$D$1", - "values": "Sheet1!$B$3:$D$3" - }, - { - "name": "Sheet1!$A$4", - "categories": "Sheet1!$B$1:$D$1", - "values": "Sheet1!$B$4:$D$4" - }], - "title": - { - "name": "Fruit 3D Clustered Column Chart" - } - }`); err != nil { + }); err != nil { fmt.Println(err) return } @@ -193,22 +192,24 @@ func main() { } }() // Insert a picture. - if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { + if err := f.AddPicture("Sheet1", "A2", "image.png", nil); err != nil { fmt.Println(err) } // Insert a picture to worksheet with scaling. + enable, disable, scale := true, false, 0.5 if err := f.AddPicture("Sheet1", "D2", "image.jpg", - `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { + &excelize.PictureOptions{XScale: &scale, YScale: &scale}); err != nil { fmt.Println(err) } // Insert a picture offset in the cell with printing support. - if err := f.AddPicture("Sheet1", "H2", "image.gif", `{ - "x_offset": 15, - "y_offset": 10, - "print_obj": true, - "lock_aspect_ratio": false, - "locked": false - }`); err != nil { + if err := f.AddPicture("Sheet1", "H2", "image.gif", + &excelize.PictureOptions{ + PrintObject: &enable, + LockAspectRatio: false, + OffsetX: 15, + OffsetY: 10, + Locked: &disable, + }); err != nil { fmt.Println(err) } // Save the spreadsheet with the origin path. diff --git a/README_zh.md b/README_zh.md index 212bf79be6..ddd892e95e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -121,41 +121,40 @@ import ( ) func main() { - categories := map[string]string{ - "A2": "Small", "A3": "Normal", "A4": "Large", - "B1": "Apple", "C1": "Orange", "D1": "Pear"} - values := map[string]int{ - "B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8} f := excelize.NewFile() - for k, v := range categories { - f.SetCellValue("Sheet1", k, v) - } - for k, v := range values { - f.SetCellValue("Sheet1", k, v) + for idx, row := range [][]interface{}{ + {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3}, + {"Normal", 5, 2, 4}, {"Large", 6, 7, 8}, + } { + cell, err := excelize.CoordinatesToCellName(1, idx+1) + if err != nil { + fmt.Println(err) + return + } + f.SetSheetRow("Sheet1", cell, &row) } - if err := f.AddChart("Sheet1", "E1", `{ - "type": "col3DClustered", - "series": [ - { - "name": "Sheet1!$A$2", - "categories": "Sheet1!$B$1:$D$1", - "values": "Sheet1!$B$2:$D$2" + if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ + Type: "col3DClustered", + Series: []excelize.ChartSeries{ + { + Name: "Sheet1!$A$2", + Categories: "Sheet1!$B$1:$D$1", + Values: "Sheet1!$B$2:$D$2", + }, + { + Name: "Sheet1!$A$3", + Categories: "Sheet1!$B$1:$D$1", + Values: "Sheet1!$B$3:$D$3", + }, + { + Name: "Sheet1!$A$4", + Categories: "Sheet1!$B$1:$D$1", + Values: "Sheet1!$B$4:$D$4", + }}, + Title: excelize.ChartTitle{ + Name: "Fruit 3D Clustered Column Chart", }, - { - "name": "Sheet1!$A$3", - "categories": "Sheet1!$B$1:$D$1", - "values": "Sheet1!$B$3:$D$3" - }, - { - "name": "Sheet1!$A$4", - "categories": "Sheet1!$B$1:$D$1", - "values": "Sheet1!$B$4:$D$4" - }], - "title": - { - "name": "Fruit 3D Clustered Column Chart" - } - }`); err != nil { + }); err != nil { fmt.Println(err) return } @@ -193,22 +192,24 @@ func main() { } }() // 插入图片 - if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { + if err := f.AddPicture("Sheet1", "A2", "image.png", nil); err != nil { fmt.Println(err) } // 在工作表中插入图片,并设置图片的缩放比例 + enable, disable, scale := true, false, 0.5 if err := f.AddPicture("Sheet1", "D2", "image.jpg", - `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { + &excelize.PictureOptions{XScale: &scale, YScale: &scale}); err != nil { fmt.Println(err) } // 在工作表中插入图片,并设置图片的打印属性 - if err := f.AddPicture("Sheet1", "H2", "image.gif", `{ - "x_offset": 15, - "y_offset": 10, - "print_obj": true, - "lock_aspect_ratio": false, - "locked": false - }`); err != nil { + if err := f.AddPicture("Sheet1", "H2", "image.gif", + &excelize.PictureOptions{ + PrintObject: &enable, + LockAspectRatio: false, + OffsetX: 15, + OffsetY: 10, + Locked: &disable, + }); err != nil { fmt.Println(err) } // 保存工作簿 diff --git a/adjust_test.go b/adjust_test.go index 3ce1796ed3..7b992419a9 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -10,7 +10,7 @@ import ( func TestAdjustMergeCells(t *testing.T) { f := NewFile() - // Test adjustAutoFilter with illegal cell reference. + // Test adjustAutoFilter with illegal cell reference assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -57,7 +57,7 @@ func TestAdjustMergeCells(t *testing.T) { }, }, columns, 1, -1)) - // Test adjustMergeCells. + // Test adjust merge cells var cases []struct { label string ws *xlsxWorksheet @@ -68,7 +68,7 @@ func TestAdjustMergeCells(t *testing.T) { expectRect []int } - // Test insert. + // Test adjust merged cell when insert rows and columns cases = []struct { label string ws *xlsxWorksheet @@ -139,7 +139,7 @@ func TestAdjustMergeCells(t *testing.T) { assert.Equal(t, c.expectRect, c.ws.MergeCells.Cells[0].rect, c.label) } - // Test delete, + // Test adjust merged cells when delete rows and columns cases = []struct { label string ws *xlsxWorksheet @@ -292,7 +292,7 @@ func TestAdjustAutoFilter(t *testing.T) { Ref: "A1:A3", }, }, rows, 1, -1)) - // Test adjustAutoFilter with illegal cell reference. + // Test adjustAutoFilter with illegal cell reference assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{ Ref: "A:B1", @@ -307,15 +307,15 @@ func TestAdjustAutoFilter(t *testing.T) { func TestAdjustTable(t *testing.T) { f, sheetName := NewFile(), "Sheet1" - for idx, tableRange := range [][]string{{"B2", "C3"}, {"E3", "F5"}, {"H5", "H8"}, {"J5", "K9"}} { - assert.NoError(t, f.AddTable(sheetName, tableRange[0], tableRange[1], fmt.Sprintf(`{ - "table_name": "table%d", - "table_style": "TableStyleMedium2", - "show_first_column": true, - "show_last_column": true, - "show_row_stripes": false, - "show_column_stripes": true - }`, idx))) + for idx, reference := range []string{"B2:C3", "E3:F5", "H5:H8", "J5:K9"} { + assert.NoError(t, f.AddTable(sheetName, reference, &TableOptions{ + Name: fmt.Sprintf("table%d", idx), + StyleName: "TableStyleMedium2", + ShowFirstColumn: true, + ShowLastColumn: true, + ShowRowStripes: boolPtr(false), + ShowColumnStripes: true, + })) } assert.NoError(t, f.RemoveRow(sheetName, 2)) assert.NoError(t, f.RemoveRow(sheetName, 3)) @@ -323,31 +323,32 @@ func TestAdjustTable(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustTable.xlsx"))) f = NewFile() - assert.NoError(t, f.AddTable(sheetName, "A1", "D5", "")) - // Test adjust table with non-table part. + assert.NoError(t, f.AddTable(sheetName, "A1:D5", nil)) + // Test adjust table with non-table part f.Pkg.Delete("xl/tables/table1.xml") assert.NoError(t, f.RemoveRow(sheetName, 1)) - // Test adjust table with unsupported charset. + // Test adjust table with unsupported charset f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) assert.NoError(t, f.RemoveRow(sheetName, 1)) - // Test adjust table with invalid table range reference. + // Test adjust table with invalid table range reference f.Pkg.Store("xl/tables/table1.xml", []byte(`
`)) assert.NoError(t, f.RemoveRow(sheetName, 1)) } func TestAdjustHelper(t *testing.T) { f := NewFile() - f.NewSheet("Sheet2") + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:B1"}}}, }) f.Sheet.Store("xl/worksheets/sheet2.xml", &xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{Ref: "A1:B"}, }) - // Test adjustHelper with illegal cell reference. + // Test adjustHelper with illegal cell reference assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) - // Test adjustHelper on not exists worksheet. + // Test adjustHelper on not exists worksheet assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN does not exist") } diff --git a/calc.go b/calc.go index 9ab5eeb2fe..aeb00fe80a 100644 --- a/calc.go +++ b/calc.go @@ -48,7 +48,7 @@ const ( formulaErrorSPILL = "#SPILL!" formulaErrorCALC = "#CALC!" formulaErrorGETTINGDATA = "#GETTING_DATA" - // formula criteria condition enumeration. + // Formula criteria condition enumeration _ byte = iota criteriaEq criteriaLe @@ -100,7 +100,7 @@ const ( ) var ( - // tokenPriority defined basic arithmetic operator priority. + // tokenPriority defined basic arithmetic operator priority tokenPriority = map[string]int{ "^": 5, "*": 4, diff --git a/calc_test.go b/calc_test.go index 1c1f4d53c3..9ebfef81c5 100644 --- a/calc_test.go +++ b/calc_test.go @@ -5478,7 +5478,8 @@ func TestCalcSLOP(t *testing.T) { func TestCalcSHEET(t *testing.T) { f := NewFile() - f.NewSheet("Sheet2") + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) formulaList := map[string]string{ "=SHEET(\"Sheet2\")": "2", "=SHEET(Sheet2!A1)": "2", @@ -5494,7 +5495,8 @@ func TestCalcSHEET(t *testing.T) { func TestCalcSHEETS(t *testing.T) { f := NewFile() - f.NewSheet("Sheet2") + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) formulaList := map[string]string{ "=SHEETS(Sheet1!A1:B1)": "1", "=SHEETS(Sheet1!A1:Sheet1!A1)": "1", diff --git a/calcchain_test.go b/calcchain_test.go index 9eec804ca9..9256d179e7 100644 --- a/calcchain_test.go +++ b/calcchain_test.go @@ -8,7 +8,7 @@ import ( func TestCalcChainReader(t *testing.T) { f := NewFile() - // Test read calculation chain with unsupported charset. + // Test read calculation chain with unsupported charset f.CalcChain = nil f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset) _, err := f.calcChainReader() @@ -34,12 +34,12 @@ func TestDeleteCalcChain(t *testing.T) { formulaType, ref := STCellFormulaTypeShared, "C1:C5" assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType})) - // Test delete calculation chain with unsupported charset calculation chain. + // Test delete calculation chain with unsupported charset calculation chain f.CalcChain = nil f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset) assert.EqualError(t, f.SetCellValue("Sheet1", "C1", true), "XML syntax error on line 1: invalid UTF-8") - // Test delete calculation chain with unsupported charset content types. + // Test delete calculation chain with unsupported charset content types f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.deleteCalcChain(1, "A1"), "XML syntax error on line 1: invalid UTF-8") diff --git a/cell.go b/cell.go index fe393ec74f..de08ffa88f 100644 --- a/cell.go +++ b/cell.go @@ -655,9 +655,9 @@ type FormulaOpts struct { // // Example 5, set range array formula "A1:A2" for the cell "A3" on "Sheet1": // -// formulaType, ref := excelize.STCellFormulaTypeArray, "A3:A3" -// err := f.SetCellFormula("Sheet1", "A3", "=A1:A2", -// excelize.FormulaOpts{Ref: &ref, Type: &formulaType}) +// formulaType, ref := excelize.STCellFormulaTypeArray, "A3:A3" +// err := f.SetCellFormula("Sheet1", "A3", "=A1:A2", +// excelize.FormulaOpts{Ref: &ref, Type: &formulaType}) // // Example 6, set shared formula "=A1+B1" for the cell "C1:C5" // on "Sheet1", "C1" is the master cell: @@ -681,12 +681,13 @@ type FormulaOpts struct { // f := excelize.NewFile() // for idx, row := range [][]interface{}{{"A", "B", "C"}, {1, 2}} { // if err := f.SetSheetRow("Sheet1", fmt.Sprintf("A%d", idx+1), &row); err != nil { -// fmt.Println(err) -// return +// fmt.Println(err) +// return // } // } -// if err := f.AddTable("Sheet1", "A1", "C2", -// `{"table_name":"Table1","table_style":"TableStyleMedium2"}`); err != nil { +// if err := f.AddTable("Sheet1", "A1:C2", &excelize.TableOptions{ +// Name: "Table1", StyleName: "TableStyleMedium2", +// }); err != nil { // fmt.Println(err) // return // } diff --git a/cell_test.go b/cell_test.go index 2a9357a774..e387793080 100644 --- a/cell_test.go +++ b/cell_test.go @@ -37,13 +37,20 @@ func TestConcurrency(t *testing.T) { uint64(1<<32 - 1), true, complex64(5 + 10i), })) // Concurrency create style - style, err := f.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`) + style, err := f.NewStyle(&Style{Font: &Font{Color: "#1265BE", Underline: "single"}}) assert.NoError(t, err) // Concurrency set cell style assert.NoError(t, f.SetCellStyle("Sheet1", "A3", "A3", style)) // Concurrency add picture assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), - `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)) + &PictureOptions{ + OffsetX: 10, + OffsetY: 10, + Hyperlink: "https://github.com/xuri/excelize", + HyperlinkType: "External", + Positioning: "oneCell", + }, + )) // Concurrency get cell picture name, raw, err := f.GetPicture("Sheet1", "A1") assert.Equal(t, "", name) @@ -556,7 +563,7 @@ func TestSetCellFormula(t *testing.T) { for idx, row := range [][]interface{}{{"A", "B", "C"}, {1, 2}} { assert.NoError(t, f.SetSheetRow("Sheet1", fmt.Sprintf("A%d", idx+1), &row)) } - assert.NoError(t, f.AddTable("Sheet1", "A1", "C2", `{"table_name":"Table1","table_style":"TableStyleMedium2"}`)) + assert.NoError(t, f.AddTable("Sheet1", "A1:C2", &TableOptions{Name: "Table1", StyleName: "TableStyleMedium2"})) formulaType = STCellFormulaTypeDataTable assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=SUM(Table1[[A]:[B]])", FormulaOpts{Type: &formulaType})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula6.xlsx"))) @@ -874,7 +881,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 value 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) diff --git a/chart.go b/chart.go index d4ea8d9918..a9d96a0172 100644 --- a/chart.go +++ b/chart.go @@ -12,7 +12,6 @@ package excelize import ( - "encoding/json" "encoding/xml" "fmt" "strconv" @@ -480,28 +479,41 @@ var ( // parseChartOptions provides a function to parse the format settings of the // chart with default value. -func parseChartOptions(opts string) (*chartOptions, error) { - options := chartOptions{ - Dimension: chartDimensionOptions{ - Width: 480, - Height: 290, - }, - Format: pictureOptions{ - FPrintsWithSheet: true, - XScale: 1, - YScale: 1, - }, - Legend: chartLegendOptions{ - Position: "bottom", - }, - Title: chartTitleOptions{ - Name: " ", - }, - VaryColors: true, - ShowBlanksAs: "gap", +func parseChartOptions(opts *Chart) (*Chart, error) { + if opts == nil { + return nil, ErrParameterInvalid + } + if opts.Dimension.Width == nil { + opts.Dimension.Width = intPtr(defaultChartDimensionWidth) + } + if opts.Dimension.Height == nil { + opts.Dimension.Height = intPtr(defaultChartDimensionHeight) + } + if opts.Format.PrintObject == nil { + opts.Format.PrintObject = boolPtr(true) + } + if opts.Format.Locked == nil { + opts.Format.Locked = boolPtr(false) + } + if opts.Format.XScale == nil { + opts.Format.XScale = float64Ptr(defaultPictureScale) } - err := json.Unmarshal([]byte(opts), &options) - return &options, err + if opts.Format.YScale == nil { + opts.Format.YScale = float64Ptr(defaultPictureScale) + } + if opts.Legend.Position == nil { + opts.Legend.Position = stringPtr(defaultChartLegendPosition) + } + if opts.Title.Name == "" { + opts.Title.Name = " " + } + if opts.VaryColors == nil { + opts.VaryColors = boolPtr(true) + } + if opts.ShowBlanksAs == "" { + opts.ShowBlanksAs = defaultChartShowBlanksAs + } + return opts, nil } // AddChart provides the method to add chart in a sheet by given chart format @@ -518,66 +530,53 @@ func parseChartOptions(opts string) (*chartOptions, error) { // ) // // func main() { -// categories := map[string]string{ -// "A2": "Small", "A3": "Normal", "A4": "Large", -// "B1": "Apple", "C1": "Orange", "D1": "Pear"} -// values := map[string]int{ -// "B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8} // f := excelize.NewFile() -// for k, v := range categories { -// f.SetCellValue("Sheet1", k, v) -// } -// for k, v := range values { -// f.SetCellValue("Sheet1", k, v) +// for idx, row := range [][]interface{}{ +// {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3}, +// {"Normal", 5, 2, 4}, {"Large", 6, 7, 8}, +// } { +// cell, err := excelize.CoordinatesToCellName(1, idx+1) +// if err != nil { +// fmt.Println(err) +// return +// } +// f.SetSheetRow("Sheet1", cell, &row) // } -// if err := f.AddChart("Sheet1", "E1", `{ -// "type": "col3DClustered", -// "series": [ -// { -// "name": "Sheet1!$A$2", -// "categories": "Sheet1!$B$1:$D$1", -// "values": "Sheet1!$B$2:$D$2" -// }, -// { -// "name": "Sheet1!$A$3", -// "categories": "Sheet1!$B$1:$D$1", -// "values": "Sheet1!$B$3:$D$3" -// }, -// { -// "name": "Sheet1!$A$4", -// "categories": "Sheet1!$B$1:$D$1", -// "values": "Sheet1!$B$4:$D$4" -// }], -// "title": -// { -// "name": "Fruit 3D Clustered Column Chart" +// positionBottom := "bottom" +// if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ +// Type: "col3DClustered", +// Series: []excelize.ChartSeries{ +// { +// Name: "Sheet1!$A$2", +// Categories: "Sheet1!$B$1:$D$1", +// Values: "Sheet1!$B$2:$D$2", +// }, +// { +// Name: "Sheet1!$A$3", +// Categories: "Sheet1!$B$1:$D$1", +// Values: "Sheet1!$B$3:$D$3", +// }, +// { +// Name: "Sheet1!$A$4", +// Categories: "Sheet1!$B$1:$D$1", +// Values: "Sheet1!$B$4:$D$4", +// }, // }, -// "legend": -// { -// "none": false, -// "position": "bottom", -// "show_legend_key": false +// Title: excelize.ChartTitle{ +// Name: "Fruit 3D Clustered Column Chart", // }, -// "plotarea": -// { -// "show_bubble_size": true, -// "show_cat_name": false, -// "show_leader_lines": false, -// "show_percent": true, -// "show_series_name": true, -// "show_val": true +// Legend: excelize.ChartLegend{ +// None: false, Position: &positionBottom, ShowLegendKey: false, // }, -// "show_blanks_as": "zero", -// "x_axis": -// { -// "reverse_order": true +// PlotArea: excelize.ChartPlotArea{ +// ShowBubbleSize: true, +// ShowCatName: false, +// ShowLeaderLines: false, +// ShowPercent: true, +// ShowSerName: true, +// ShowVal: true, // }, -// "y_axis": -// { -// "maximum": 7.5, -// "minimum": 0.5 -// } -// }`); err != nil { +// }); err != nil { // fmt.Println(err) // return // } @@ -651,21 +650,21 @@ func parseChartOptions(opts string) (*chartOptions, error) { // // The series options that can be set are: // -// name -// categories -// values -// line -// marker +// Name +// Categories +// Values +// Line +// Marker // -// name: Set the name for the series. The name is displayed in the chart legend and in the formula bar. The name property is optional and if it isn't supplied it will default to Series 1..n. The name can also be a formula such as Sheet1!$A$1 +// Name: Set the name for the series. The name is displayed in the chart legend and in the formula bar. The 'Name' property is optional and if it isn't supplied it will default to Series 1..n. The name can also be a formula such as Sheet1!$A$1 // -// categories: This sets the chart category labels. The category is more or less the same as the X axis. In most chart types the categories property is optional and the chart will just assume a sequential series from 1..n. +// Categories: This sets the chart category labels. The category is more or less the same as the X axis. In most chart types the 'Categories' property is optional and the chart will just assume a sequential series from 1..n. // -// values: This is the most important property of a series and is the only mandatory option for every chart object. This option links the chart with the worksheet data that it displays. +// Values: This is the most important property of a series and is the only mandatory option for every chart object. This option links the chart with the worksheet data that it displays. // -// line: This sets the line format of the line chart. The line property is optional and if it isn't supplied it will default style. The options that can be set are width and color. The range of width is 0.25pt - 999pt. If the value of width is outside the range, the default width of the line is 2pt. The value for color should be represented in hex format (e.g., #000000 - #FFFFFF) +// Line: This sets the line format of the line chart. The 'Line' property is optional and if it isn't supplied it will default style. The options that can be set are width and color. The range of width is 0.25pt - 999pt. If the value of width is outside the range, the default width of the line is 2pt. The value for color should be represented in hex format (e.g., #000000 - #FFFFFF) // -// marker: This sets the marker of the line chart and scatter chart. The range of optional field 'size' is 2-72 (default value is 5). The enumeration value of optional field 'symbol' are (default value is 'auto'): +// Marker: This sets the marker of the line chart and scatter chart. The range of optional field 'size' is 2-72 (default value is 5). The enumeration value of optional field 'Symbol' are (default value is 'auto'): // // circle // dash @@ -682,13 +681,13 @@ func parseChartOptions(opts string) (*chartOptions, error) { // // Set properties of the chart legend. The options that can be set are: // -// none -// position -// show_legend_key +// None +// Position +// ShowLegendKey // -// none: Specified if show the legend without overlapping the chart. The default value is 'false'. +// None: Specified if show the legend without overlapping the chart. The default value is 'false'. // -// position: Set the position of the chart legend. The default legend position is right. This parameter only takes effect when 'none' is false. The available positions are: +// Position: Set the position of the chart legend. The default legend position is right. This parameter only takes effect when 'none' is false. The available positions are: // // top // bottom @@ -696,15 +695,15 @@ func parseChartOptions(opts string) (*chartOptions, error) { // right // top_right // -// show_legend_key: Set the legend keys shall be shown in data labels. The default value is false. +// ShowLegendKey: Set the legend keys shall be shown in data labels. The default value is false. // // Set properties of the chart title. The properties that can be set are: // -// title +// Title // -// name: Set the name (title) for the chart. The name is displayed above the chart. The name can also be a formula such as Sheet1!$A$1 or a list with a sheet name. The name property is optional. The default is to have no chart title. +// Name: Set the name (title) for the chart. The name is displayed above the chart. The name can also be a formula such as Sheet1!$A$1 or a list with a sheet name. The name property is optional. The default is to have no chart title. // -// Specifies how blank cells are plotted on the chart by show_blanks_as. The default value is gap. The options that can be set are: +// Specifies how blank cells are plotted on the chart by ShowBlanksAs. The default value is gap. The options that can be set are: // // gap // span @@ -716,80 +715,80 @@ func parseChartOptions(opts string) (*chartOptions, error) { // // zero: Specifies that blank values shall be treated as zero. // -// Specifies that each data marker in the series has a different color by vary_colors. The default value is true. +// Specifies that each data marker in the series has a different color by VaryColors. The default value is true. // // Set chart offset, scale, aspect ratio setting and print settings by format, same as function AddPicture. // -// Set the position of the chart plot area by plotarea. The properties that can be set are: +// Set the position of the chart plot area by PlotArea. The properties that can be set are: // -// show_bubble_size -// show_cat_name -// show_leader_lines -// show_percent -// show_series_name -// show_val +// ShowBubbleSize +// ShowCatName +// ShowLeaderLines +// ShowPercent +// ShowSerName +// ShowVal // -// show_bubble_size: Specifies the bubble size shall be shown in a data label. The show_bubble_size property is optional. The default value is false. +// ShowBubbleSize: Specifies the bubble size shall be shown in a data label. The ShowBubbleSize property is optional. The default value is false. // -// show_cat_name: Specifies that the category name shall be shown in the data label. The show_cat_name property is optional. The default value is true. +// ShowCatName: Specifies that the category name shall be shown in the data label. The ShowCatName property is optional. The default value is true. // -// show_leader_lines: Specifies leader lines shall be shown for data labels. The show_leader_lines property is optional. The default value is false. +// ShowLeaderLines: Specifies leader lines shall be shown for data labels. The ShowLeaderLines property is optional. The default value is false. // -// show_percent: Specifies that the percentage shall be shown in a data label. The show_percent property is optional. The default value is false. +// ShowPercent: Specifies that the percentage shall be shown in a data label. The ShowPercent property is optional. The default value is false. // -// show_series_name: Specifies that the series name shall be shown in a data label. The show_series_name property is optional. The default value is false. +// ShowSerName: Specifies that the series name shall be shown in a data label. The ShowSerName property is optional. The default value is false. // -// show_val: Specifies that the value shall be shown in a data label. The show_val property is optional. The default value is false. +// ShowVal: Specifies that the value shall be shown in a data label. The ShowVal property is optional. The default value is false. // -// Set the primary horizontal and vertical axis options by x_axis and y_axis. The properties of x_axis that can be set are: +// Set the primary horizontal and vertical axis options by XAxis and YAxis. The properties of XAxis that can be set are: // -// none -// major_grid_lines -// minor_grid_lines -// tick_label_skip -// reverse_order -// maximum -// minimum -// font +// None +// MajorGridLines +// MinorGridLines +// TickLabelSkip +// ReverseOrder +// Maximum +// Minimum +// Font // -// The properties of y_axis that can be set are: +// The properties of YAxis that can be set are: // -// none -// major_grid_lines -// minor_grid_lines -// major_unit -// tick_label_skip -// reverse_order -// maximum -// minimum -// font +// None +// MajorGridLines +// MinorGridLines +// MajorUnit +// TickLabelSkip +// ReverseOrder +// Maximum +// Minimum +// Font // // none: Disable axes. // -// major_grid_lines: Specifies major grid lines. +// MajorGridLines: Specifies major grid lines. // -// minor_grid_lines: Specifies minor grid lines. +// MinorGridLines: Specifies minor grid lines. // -// major_unit: Specifies the distance between major ticks. Shall contain a positive floating-point number. The major_unit property is optional. The default value is auto. +// MajorUnit: Specifies the distance between major ticks. Shall contain a positive floating-point number. The MajorUnit property is optional. The default value is auto. // -// tick_label_skip: Specifies how many tick labels to skip between label that is drawn. The tick_label_skip property is optional. The default value is auto. +// TickLabelSkip: Specifies how many tick labels to skip between label that is drawn. The TickLabelSkip property is optional. The default value is auto. // -// reverse_order: Specifies that the categories or values on reverse order (orientation of the chart). The reverse_order property is optional. The default value is false. +// ReverseOrder: Specifies that the categories or values on reverse order (orientation of the chart). The ReverseOrder property is optional. The default value is false. // -// maximum: Specifies that the fixed maximum, 0 is auto. The maximum property is optional. The default value is auto. +// Maximum: Specifies that the fixed maximum, 0 is auto. The Maximum property is optional. The default value is auto. // -// minimum: Specifies that the fixed minimum, 0 is auto. The minimum property is optional. The default value is auto. +// Minimum: Specifies that the fixed minimum, 0 is auto. The Minimum property is optional. The default value is auto. // -// font: Specifies that the font of the horizontal and vertical axis. The properties of font that can be set are: +// Font: Specifies that the font of the horizontal and vertical axis. The properties of font that can be set are: // -// bold -// italic -// underline -// family -// size -// strike -// color -// vertAlign +// Bold +// Italic +// Underline +// Family +// Size +// Strike +// Color +// VertAlign // // Set chart size by dimension property. The dimension property is optional. The default width is 480, and height is 290. // @@ -806,112 +805,100 @@ func parseChartOptions(opts string) (*chartOptions, error) { // ) // // func main() { -// categories := map[string]string{ -// "A2": "Small", "A3": "Normal", "A4": "Large", -// "B1": "Apple", "C1": "Orange", "D1": "Pear"} -// values := map[string]int{ -// "B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8} // f := excelize.NewFile() -// for k, v := range categories { -// f.SetCellValue("Sheet1", k, v) -// } -// for k, v := range values { -// f.SetCellValue("Sheet1", k, v) +// for idx, row := range [][]interface{}{ +// {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3}, +// {"Normal", 5, 2, 4}, {"Large", 6, 7, 8}, +// } { +// cell, err := excelize.CoordinatesToCellName(1, idx+1) +// if err != nil { +// fmt.Println(err) +// return +// } +// f.SetSheetRow("Sheet1", cell, &row) // } -// if err := f.AddChart("Sheet1", "E1", `{ -// "type": "col", -// "series": [ -// { -// "name": "Sheet1!$A$2", -// "categories": "", -// "values": "Sheet1!$B$2:$D$2" +// enable, disable, scale := true, false, 1.0 +// positionLeft, positionRight := "left", "right" +// if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ +// Type: "col", +// Series: []excelize.ChartSeries{ +// { +// Name: "Sheet1!$A$2", +// Categories: "Sheet1!$B$1:$D$1", +// Values: "Sheet1!$B$2:$D$2", +// }, // }, -// { -// "name": "Sheet1!$A$3", -// "categories": "Sheet1!$B$1:$D$1", -// "values": "Sheet1!$B$3:$D$3" -// }], -// "format": -// { -// "x_scale": 1.0, -// "y_scale": 1.0, -// "x_offset": 15, -// "y_offset": 10, -// "print_obj": true, -// "lock_aspect_ratio": false, -// "locked": false +// Format: excelize.Picture{ +// XScale: &scale, +// YScale: &scale, +// OffsetX: 15, +// OffsetY: 10, +// PrintObject: &enable, +// LockAspectRatio: false, +// Locked: &disable, // }, -// "title": -// { -// "name": "Clustered Column - Line Chart" +// Title: excelize.ChartTitle{ +// Name: "Clustered Column - Line Chart", // }, -// "legend": -// { -// "position": "left", -// "show_legend_key": false +// Legend: excelize.ChartLegend{ +// Position: &positionLeft, ShowLegendKey: false, // }, -// "plotarea": -// { -// "show_bubble_size": true, -// "show_cat_name": false, -// "show_leader_lines": false, -// "show_percent": true, -// "show_series_name": true, -// "show_val": true -// } -// }`, `{ -// "type": "line", -// "series": [ -// { -// "name": "Sheet1!$A$4", -// "categories": "Sheet1!$B$1:$D$1", -// "values": "Sheet1!$B$4:$D$4", -// "marker": +// PlotArea: excelize.ChartPlotArea{ +// ShowBubbleSize: true, +// ShowCatName: false, +// ShowLeaderLines: false, +// ShowPercent: true, +// ShowSerName: true, +// ShowVal: true, +// }, +// }, &excelize.Chart{ +// Type: "line", +// Series: []excelize.ChartSeries{ // { -// "symbol": "none", -// "size": 10 -// } -// }], -// "format": -// { -// "x_scale": 1, -// "y_scale": 1, -// "x_offset": 15, -// "y_offset": 10, -// "print_obj": true, -// "lock_aspect_ratio": false, -// "locked": false +// Name: "Sheet1!$A$4", +// Categories: "Sheet1!$B$1:$D$1", +// Values: "Sheet1!$B$4:$D$4", +// Marker: excelize.ChartMarker{ +// Symbol: "none", Size: 10, +// }, +// }, // }, -// "legend": -// { -// "position": "right", -// "show_legend_key": false +// Format: excelize.Picture{ +// XScale: &scale, +// YScale: &scale, +// OffsetX: 15, +// OffsetY: 10, +// PrintObject: &enable, +// LockAspectRatio: false, +// Locked: &disable, // }, -// "plotarea": -// { -// "show_bubble_size": true, -// "show_cat_name": false, -// "show_leader_lines": false, -// "show_percent": true, -// "show_series_name": true, -// "show_val": true -// } -// }`); err != nil { +// Legend: excelize.ChartLegend{ +// Position: &positionRight, ShowLegendKey: false, +// }, +// PlotArea: excelize.ChartPlotArea{ +// ShowBubbleSize: true, +// ShowCatName: false, +// ShowLeaderLines: false, +// ShowPercent: true, +// ShowSerName: true, +// ShowVal: true, +// }, +// }); err != nil { // fmt.Println(err) // return // } -// // Save spreadsheet file by the given path. +// // Save spreadsheet by the given path. // if err := f.SaveAs("Book1.xlsx"); err != nil { // fmt.Println(err) // } // } -func (f *File) AddChart(sheet, cell, opts string, combo ...string) error { - // Read sheet data. +func (f *File) AddChart(sheet, cell string, chart *Chart, combo ...*Chart) error { + // Read worksheet data ws, err := f.workSheetReader(sheet) if err != nil { return err } - options, comboCharts, err := f.getChartOptions(opts, combo) + opts, comboCharts, err := f.getChartOptions(chart, combo) if err != nil { return err } @@ -922,11 +909,11 @@ func (f *File) AddChart(sheet, cell, opts string, combo ...string) error { drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML) drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") - err = f.addDrawingChart(sheet, drawingXML, cell, options.Dimension.Width, options.Dimension.Height, drawingRID, &options.Format) + err = f.addDrawingChart(sheet, drawingXML, cell, *opts.Dimension.Width, *opts.Dimension.Height, drawingRID, &opts.Format) if err != nil { return err } - f.addChart(options, comboCharts) + f.addChart(opts, comboCharts) if err = f.addContentTypePart(chartID, "chart"); err != nil { return err } @@ -939,7 +926,7 @@ func (f *File) AddChart(sheet, cell, opts string, combo ...string) error { // format set (such as offset, scale, aspect ratio setting and print settings) // and properties set. In Excel a chartsheet is a worksheet that only contains // a chart. -func (f *File) AddChartSheet(sheet, opts string, combo ...string) error { +func (f *File) AddChartSheet(sheet string, chart *Chart, combo ...*Chart) error { // Check if the worksheet already exists idx, err := f.GetSheetIndex(sheet) if err != nil { @@ -948,7 +935,7 @@ func (f *File) AddChartSheet(sheet, opts string, combo ...string) error { if idx != -1 { return ErrExistsSheet } - options, comboCharts, err := f.getChartOptions(opts, combo) + opts, comboCharts, err := f.getChartOptions(chart, combo) if err != nil { return err } @@ -975,10 +962,10 @@ func (f *File) AddChartSheet(sheet, opts string, combo ...string) error { f.prepareChartSheetDrawing(&cs, drawingID, sheet) drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") - if err = f.addSheetDrawingChart(drawingXML, drawingRID, &options.Format); err != nil { + if err = f.addSheetDrawingChart(drawingXML, drawingRID, &opts.Format); err != nil { return err } - f.addChart(options, comboCharts) + f.addChart(opts, comboCharts) if err = f.addContentTypePart(chartID, "chart"); err != nil { return err } @@ -996,8 +983,8 @@ func (f *File) AddChartSheet(sheet, opts string, combo ...string) error { // getChartOptions provides a function to check format set of the chart and // create chart format. -func (f *File) getChartOptions(opts string, combo []string) (*chartOptions, []*chartOptions, error) { - var comboCharts []*chartOptions +func (f *File) getChartOptions(opts *Chart, combo []*Chart) (*Chart, []*Chart, error) { + var comboCharts []*Chart options, err := parseChartOptions(opts) if err != nil { return options, comboCharts, err diff --git a/chart_test.go b/chart_test.go index a61f1d7918..deed7dd98e 100644 --- a/chart_test.go +++ b/chart_test.go @@ -41,11 +41,20 @@ func TestChartSize(t *testing.T) { assert.NoError(t, f.SetCellValue(sheet1, cell, v)) } - assert.NoError(t, f.AddChart("Sheet1", "E4", `{"type":"col3DClustered","dimension":{"width":640, "height":480},`+ - `"series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},`+ - `{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},`+ - `{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],`+ - `"title":{"name":"3D Clustered Column Chart"}}`)) + width, height := 640, 480 + assert.NoError(t, f.AddChart("Sheet1", "E4", &Chart{ + Type: "col3DClustered", + Dimension: ChartDimension{ + Width: &width, + Height: &height, + }, + Series: []ChartSeries{ + {Name: "Sheet1!$A$2", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$2:$D$2"}, + {Name: "Sheet1!$A$3", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$3:$D$3"}, + {Name: "Sheet1!$A$4", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4"}, + }, + Title: ChartTitle{Name: "3D Clustered Column Chart"}, + })) var buffer bytes.Buffer @@ -98,14 +107,14 @@ func TestAddDrawingChart(t *testing.T) { path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) - assert.EqualError(t, f.addDrawingChart("Sheet1", path, "A1", 0, 0, 0, &pictureOptions{}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addDrawingChart("Sheet1", path, "A1", 0, 0, 0, &PictureOptions{PrintObject: boolPtr(true), Locked: boolPtr(false), XScale: float64Ptr(defaultPictureScale), YScale: float64Ptr(defaultPictureScale)}), "XML syntax error on line 1: invalid UTF-8") } func TestAddSheetDrawingChart(t *testing.T) { f := NewFile() path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) - assert.EqualError(t, f.addSheetDrawingChart(path, 0, &pictureOptions{}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addSheetDrawingChart(path, 0, &PictureOptions{PrintObject: boolPtr(true), Locked: boolPtr(false), XScale: float64Ptr(defaultPictureScale), YScale: float64Ptr(defaultPictureScale)}), "XML syntax error on line 1: invalid UTF-8") } func TestDeleteDrawing(t *testing.T) { @@ -129,75 +138,124 @@ func TestAddChart(t *testing.T) { for k, v := range values { assert.NoError(t, f.SetCellValue("Sheet1", k, v)) } - assert.EqualError(t, f.AddChart("Sheet1", "P1", ""), "unexpected end of JSON input") - - // Test add chart on not exists worksheet. - assert.EqualError(t, f.AddChart("SheetN", "P1", "{}"), "sheet SheetN does not exist") - - assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"none":true,"show_legend_key":true},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"font":{"bold":true,"italic":true,"underline":"dbl","color":"#000000"}},"y_axis":{"font":{"bold":false,"italic":false,"underline":"sng","color":"#777777"}}}`)) - assert.NoError(t, f.AddChart("Sheet1", "X1", `{"type":"colStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "P16", `{"type":"colPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"100% Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "X16", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"3D Clustered Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "P30", `{"type":"col3DStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "X30", `{"type":"col3DPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "X45", `{"type":"radar","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top_right","show_legend_key":false},"title":{"name":"Radar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"span"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AF1", `{"type":"col3DConeStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cone Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AF16", `{"type":"col3DConeClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cone Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AF30", `{"type":"col3DConePercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cone Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AF45", `{"type":"col3DCone","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cone Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AN1", `{"type":"col3DPyramidStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AN16", `{"type":"col3DPyramidClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AN30", `{"type":"col3DPyramidPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AN45", `{"type":"col3DPyramid","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AV1", `{"type":"col3DCylinderStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AV16", `{"type":"col3DCylinderClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AV30", `{"type":"col3DCylinderPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "AV45", `{"type":"col3DCylinder","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet1", "P45", `{"type":"col3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P1", `{"type":"line3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30","marker":{"symbol":"none","size":10}, "line":{"color":"#000000"}},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top","show_legend_key":false},"title":{"name":"3D Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true,"minor_grid_lines":true,"tick_label_skip":1},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1}}`)) - assert.NoError(t, f.AddChart("Sheet2", "X1", `{"type":"scatter","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"Scatter Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"right","show_legend_key":false},"title":{"name":"Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero","hole_size":30}`)) - assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30","marker":{"symbol":"none","size":10}, "line":{"color":"#000000"}},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top","show_legend_key":false},"title":{"name":"Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true,"minor_grid_lines":true,"tick_label_skip":1},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1}}`)) - assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"3D Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"gap"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P48", `{"type":"bar","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Clustered Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X48", `{"type":"barStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P64", `{"type":"barPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked 100% Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X64", `{"type":"bar3DClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Clustered Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P80", `{"type":"bar3DStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Stacked Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","y_axis":{"maximum":7.5,"minimum":0.5}}`)) - assert.NoError(t, f.AddChart("Sheet2", "X80", `{"type":"bar3DPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"reverse_order":true,"minimum":0},"y_axis":{"reverse_order":true,"maximum":0,"minimum":0}}`)) - // area series charts - assert.NoError(t, f.AddChart("Sheet2", "AF1", `{"type":"area","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AN1", `{"type":"areaStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AF16", `{"type":"areaPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D 100% Stacked Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AN16", `{"type":"area3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AF32", `{"type":"area3DStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Stacked Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AN32", `{"type":"area3DPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - // cylinder series chart - assert.NoError(t, f.AddChart("Sheet2", "AF48", `{"type":"bar3DCylinderStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AF64", `{"type":"bar3DCylinderClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AF80", `{"type":"bar3DCylinderPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - // cone series chart - assert.NoError(t, f.AddChart("Sheet2", "AN48", `{"type":"bar3DConeStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AN64", `{"type":"bar3DConeClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AN80", `{"type":"bar3DConePercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AV48", `{"type":"bar3DPyramidStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AV64", `{"type":"bar3DPyramidClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AV80", `{"type":"bar3DPyramidPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - // surface series chart - assert.NoError(t, f.AddChart("Sheet2", "AV1", `{"type":"surface3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Surface Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","y_axis":{"major_grid_lines":true}}`)) - assert.NoError(t, f.AddChart("Sheet2", "AV16", `{"type":"wireframeSurface3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Wireframe Surface Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","y_axis":{"major_grid_lines":true}}`)) - assert.NoError(t, f.AddChart("Sheet2", "AV32", `{"type":"contour","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Contour Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "BD1", `{"type":"wireframeContour","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Wireframe Contour Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - // bubble chart - assert.NoError(t, f.AddChart("Sheet2", "BD16", `{"type":"bubble","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bubble Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "BD32", `{"type":"bubble3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bubble 3D Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`)) - // pie of pie chart - assert.NoError(t, f.AddChart("Sheet2", "BD48", `{"type":"pieOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Pie of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`)) - // bar of pie chart - assert.NoError(t, f.AddChart("Sheet2", "BD64", `{"type":"barOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bar of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`)) + assert.EqualError(t, f.AddChart("Sheet1", "P1", nil), ErrParameterInvalid.Error()) + + // Test add chart on not exists worksheet + assert.EqualError(t, f.AddChart("SheetN", "P1", nil), "sheet SheetN does not exist") + positionLeft, positionBottom, positionRight, positionTop, positionTopRight := "left", "bottom", "right", "top", "top_right" + maximum, minimum, zero := 7.5, 0.5, .0 + series := []ChartSeries{ + {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}, + {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31"}, + {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32"}, + {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33"}, + {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34"}, + {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35"}, + {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36"}, + {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"}, + } + series2 := []ChartSeries{ + {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", Marker: ChartMarker{Symbol: "none", Size: 10}, Line: ChartLine{Color: "#000000"}}, + {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31"}, + {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32"}, + {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33"}, + {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34"}, + {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35"}, + {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36"}, + {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Line: ChartLine{Width: 0.25}}, + } + series3 := []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}} + format := PictureOptions{ + XScale: float64Ptr(defaultPictureScale), + YScale: float64Ptr(defaultPictureScale), + OffsetX: 15, + OffsetY: 10, + PrintObject: boolPtr(true), + LockAspectRatio: false, + Locked: boolPtr(false), + } + legend := ChartLegend{Position: &positionLeft, ShowLegendKey: false} + plotArea := ChartPlotArea{ + ShowBubbleSize: true, + ShowCatName: true, + ShowLeaderLines: false, + ShowPercent: true, + ShowSerName: true, + ShowVal: true, + } + for _, c := range []struct { + sheetName, cell string + opts *Chart + }{ + {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: "col", Series: series, Format: format, Legend: ChartLegend{None: true, ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "#000000"}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "#777777"}}}}, + {sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: "colStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: "colPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: "col3DClustered", Series: series, Format: format, Legend: ChartLegend{Position: &positionBottom, ShowLegendKey: false}, Title: ChartTitle{Name: "3D Clustered Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: "col3DStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: "col3DPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "X45", opts: &Chart{Type: "radar", Series: series, Format: format, Legend: ChartLegend{Position: &positionTopRight, ShowLegendKey: false}, Title: ChartTitle{Name: "Radar Chart"}, PlotArea: plotArea, ShowBlanksAs: "span"}}, + {sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: "col3DConeStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AF16", opts: &Chart{Type: "col3DConeClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AF30", opts: &Chart{Type: "col3DConePercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AF45", opts: &Chart{Type: "col3DCone", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AN1", opts: &Chart{Type: "col3DPyramidStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AN16", opts: &Chart{Type: "col3DPyramidClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AN30", opts: &Chart{Type: "col3DPyramidPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AN45", opts: &Chart{Type: "col3DPyramid", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AV1", opts: &Chart{Type: "col3DCylinderStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AV16", opts: &Chart{Type: "col3DCylinderClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: "col3DCylinderPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AV45", opts: &Chart{Type: "col3DCylinder", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: "col3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: "line3D", Series: series2, Format: format, Legend: ChartLegend{Position: &positionTop, ShowLegendKey: false}, Title: ChartTitle{Name: "3D Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}}, + {sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: "scatter", Series: series, Format: format, Legend: ChartLegend{Position: &positionBottom, ShowLegendKey: false}, Title: ChartTitle{Name: "Scatter Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: "doughnut", Series: series3, Format: format, Legend: ChartLegend{Position: &positionRight, ShowLegendKey: false}, Title: ChartTitle{Name: "Doughnut Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}}, + {sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: "line", Series: series2, Format: format, Legend: ChartLegend{Position: &positionTop, ShowLegendKey: false}, Title: ChartTitle{Name: "Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}}, + {sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: "pie3D", Series: series3, Format: format, Legend: ChartLegend{Position: &positionBottom, ShowLegendKey: false}, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: "pie", Series: series3, Format: format, Legend: ChartLegend{Position: &positionBottom, ShowLegendKey: false}, Title: ChartTitle{Name: "Pie Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "gap"}}, + // bar series chart + {sheetName: "Sheet2", cell: "P48", opts: &Chart{Type: "bar", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "X48", opts: &Chart{Type: "barStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "P64", opts: &Chart{Type: "barPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked 100% Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "X64", opts: &Chart{Type: "bar3DClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "P80", opts: &Chart{Type: "bar3DStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{Maximum: &maximum, Minimum: &minimum}}}, + {sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: "bar3DPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}}, + // area series chart + {sheetName: "Sheet2", cell: "AF1", opts: &Chart{Type: "area", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN1", opts: &Chart{Type: "areaStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF16", opts: &Chart{Type: "areaPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D 100% Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN16", opts: &Chart{Type: "area3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF32", opts: &Chart{Type: "area3DStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN32", opts: &Chart{Type: "area3DPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + // cylinder series chart + {sheetName: "Sheet2", cell: "AF48", opts: &Chart{Type: "bar3DCylinderStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF64", opts: &Chart{Type: "bar3DCylinderClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF80", opts: &Chart{Type: "bar3DCylinderPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + // cone series chart + {sheetName: "Sheet2", cell: "AN48", opts: &Chart{Type: "bar3DConeStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN64", opts: &Chart{Type: "bar3DConeClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN80", opts: &Chart{Type: "bar3DConePercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AV48", opts: &Chart{Type: "bar3DPyramidStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AV64", opts: &Chart{Type: "bar3DPyramidClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AV80", opts: &Chart{Type: "bar3DPyramidPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + // surface series chart + {sheetName: "Sheet2", cell: "AV1", opts: &Chart{Type: "surface3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Surface Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}}, + {sheetName: "Sheet2", cell: "AV16", opts: &Chart{Type: "wireframeSurface3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Wireframe Surface Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}}, + {sheetName: "Sheet2", cell: "AV32", opts: &Chart{Type: "contour", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "BD1", opts: &Chart{Type: "wireframeContour", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Wireframe Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + // bubble chart + {sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: "bubble", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: "bubble3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, + // pie of pie chart + {sheetName: "Sheet2", cell: "BD48", opts: &Chart{Type: "pieOfPie", Series: series3, Format: format, Legend: legend, Title: ChartTitle{Name: "Pie of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, + // bar of pie chart + {sheetName: "Sheet2", cell: "BD64", opts: &Chart{Type: "barOfPie", Series: series3, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, + } { + assert.NoError(t, f.AddChart(c.sheetName, c.cell, c.opts)) + } // combo chart - f.NewSheet("Combo Charts") + _, err = f.NewSheet("Combo Charts") + assert.NoError(t, err) clusteredColumnCombo := [][]string{ {"A1", "line", "Clustered Column - Line Chart"}, {"I1", "bubble", "Clustered Column - Bubble Chart"}, @@ -205,7 +263,7 @@ func TestAddChart(t *testing.T) { {"Y1", "doughnut", "Clustered Column - Doughnut Chart"}, } for _, props := range clusteredColumnCombo { - assert.NoError(t, f.AddChart("Combo Charts", props[0], fmt.Sprintf(`{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[2]), fmt.Sprintf(`{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[1]))) + assert.NoError(t, f.AddChart("Combo Charts", props[0], &Chart{Type: "col", Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[2]}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1], Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) } stackedAreaCombo := map[string][]string{ "A16": {"line", "Stacked Area - Line Chart"}, @@ -214,25 +272,25 @@ func TestAddChart(t *testing.T) { "Y16": {"doughnut", "Stacked Area - Doughnut Chart"}, } for axis, props := range stackedAreaCombo { - assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"areaStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[1]), fmt.Sprintf(`{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[0]))) + assert.NoError(t, f.AddChart("Combo Charts", axis, &Chart{Type: "areaStacked", Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[1]}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[0], Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) // Test with invalid sheet name - assert.EqualError(t, f.AddChart("Sheet:1", "A1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}]}`), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddChart("Sheet:1", "A1", &Chart{Type: "col", Series: series[:1]}), ErrSheetNameInvalid.Error()) // Test with illegal cell reference - assert.EqualError(t, f.AddChart("Sheet2", "A", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.AddChart("Sheet2", "A", &Chart{Type: "col", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // Test with unsupported chart type - assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"unknown","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bubble 3D Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`), "unsupported chart type unknown") + assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: "unknown", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}), "unsupported chart type unknown") // Test add combo chart with invalid format set - assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`, ""), "unexpected end of JSON input") + assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: "col", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}, nil), ErrParameterInvalid.Error()) // Test add combo chart with unsupported chart type - assert.EqualError(t, f.AddChart("Sheet2", "BD64", `{"type":"barOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bar of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`, `{"type":"unknown","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bar of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`), "unsupported chart type unknown") + assert.EqualError(t, f.AddChart("Sheet2", "BD64", &Chart{Type: "barOfPie", Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}, &Chart{Type: "unknown", Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}), "unsupported chart type unknown") assert.NoError(t, f.Close()) // Test add chart with unsupported charset content types. f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"title":{"name":"2D Column Chart"}}`), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddChart("Sheet1", "P1", &Chart{Type: "col", Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: ChartTitle{Name: "2D Column Chart"}}), "XML syntax error on line 1: invalid UTF-8") } func TestAddChartSheet(t *testing.T) { @@ -245,7 +303,12 @@ func TestAddChartSheet(t *testing.T) { for k, v := range values { assert.NoError(t, f.SetCellValue("Sheet1", k, v)) } - assert.NoError(t, f.AddChartSheet("Chart1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`)) + series := []ChartSeries{ + {Name: "Sheet1!$A$2", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$2:$D$2"}, + {Name: "Sheet1!$A$3", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$3:$D$3"}, + {Name: "Sheet1!$A$4", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4"}, + } + assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: "col3DClustered", Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}})) // Test set the chartsheet as active sheet var sheetIdx int for idx, sheetName := range f.GetSheetList() { @@ -259,11 +322,12 @@ func TestAddChartSheet(t *testing.T) { // Test cell value on chartsheet assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is not a worksheet") // Test add chartsheet on already existing name sheet - assert.EqualError(t, f.AddChartSheet("Sheet1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`), ErrExistsSheet.Error()) + + assert.EqualError(t, f.AddChartSheet("Sheet1", &Chart{Type: "col3DClustered", Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), ErrExistsSheet.Error()) // Test add chartsheet with invalid sheet name - assert.EqualError(t, f.AddChartSheet("Sheet:1", "A1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddChartSheet("Sheet:1", nil, &Chart{Type: "col3DClustered", Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), ErrSheetNameInvalid.Error()) // Test with unsupported chart type - assert.EqualError(t, f.AddChartSheet("Chart2", `{"type":"unknown","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`), "unsupported chart type unknown") + assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: "unknown", Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), "unsupported chart type unknown") assert.NoError(t, f.UpdateLinkedValue()) @@ -272,14 +336,43 @@ func TestAddChartSheet(t *testing.T) { f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddChartSheet("Chart4", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"title":{"name":"2D Column Chart"}}`), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddChartSheet("Chart4", &Chart{Type: "col", Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: ChartTitle{Name: "2D Column Chart"}}), "XML syntax error on line 1: invalid UTF-8") } func TestDeleteChart(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) assert.NoError(t, f.DeleteChart("Sheet1", "A1")) - assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`)) + positionLeft := "left" + series := []ChartSeries{ + {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}, + {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31"}, + {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32"}, + {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33"}, + {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34"}, + {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35"}, + {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36"}, + {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"}, + } + format := PictureOptions{ + XScale: float64Ptr(defaultPictureScale), + YScale: float64Ptr(defaultPictureScale), + OffsetX: 15, + OffsetY: 10, + PrintObject: boolPtr(true), + LockAspectRatio: false, + Locked: boolPtr(false), + } + legend := ChartLegend{Position: &positionLeft, ShowLegendKey: false} + plotArea := ChartPlotArea{ + ShowBubbleSize: true, + ShowCatName: true, + ShowLeaderLines: false, + ShowPercent: true, + ShowSerName: true, + ShowVal: true, + } + assert.NoError(t, f.AddChart("Sheet1", "P1", &Chart{Type: "col", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"})) assert.NoError(t, f.DeleteChart("Sheet1", "P1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteChart.xlsx"))) // Test delete chart with invalid sheet name @@ -322,37 +415,22 @@ func TestChartWithLogarithmicBase(t *testing.T) { for cell, v := range categories { assert.NoError(t, f.SetCellValue(sheet1, cell, v)) } - - // Add two chart, one without and one with log scaling - assert.NoError(t, f.AddChart(sheet1, "C1", - `{"type":"line","dimension":{"width":640, "height":480},`+ - `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+ - `"title":{"name":"Line chart without log scaling"}}`)) - assert.NoError(t, f.AddChart(sheet1, "M1", - `{"type":"line","dimension":{"width":640, "height":480},`+ - `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+ - `"y_axis":{"logbase":10.5},`+ - `"title":{"name":"Line chart with log 10 scaling"}}`)) - assert.NoError(t, f.AddChart(sheet1, "A25", - `{"type":"line","dimension":{"width":320, "height":240},`+ - `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+ - `"y_axis":{"logbase":1.9},`+ - `"title":{"name":"Line chart with log 1.9 scaling"}}`)) - assert.NoError(t, f.AddChart(sheet1, "F25", - `{"type":"line","dimension":{"width":320, "height":240},`+ - `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+ - `"y_axis":{"logbase":2},`+ - `"title":{"name":"Line chart with log 2 scaling"}}`)) - assert.NoError(t, f.AddChart(sheet1, "K25", - `{"type":"line","dimension":{"width":320, "height":240},`+ - `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+ - `"y_axis":{"logbase":1000.1},`+ - `"title":{"name":"Line chart with log 1000.1 scaling"}}`)) - assert.NoError(t, f.AddChart(sheet1, "P25", - `{"type":"line","dimension":{"width":320, "height":240},`+ - `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+ - `"y_axis":{"logbase":1000},`+ - `"title":{"name":"Line chart with log 1000 scaling"}}`)) + series := []ChartSeries{{Name: "value", Categories: "Sheet1!$A$1:$A$19", Values: "Sheet1!$B$1:$B$10"}} + dimension := []int{640, 480, 320, 240} + for _, c := range []struct { + cell string + opts *Chart + }{ + {cell: "C1", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[0], Height: &dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart without log scaling"}}}, + {cell: "M1", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[0], Height: &dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart with log 10.5 scaling"}, YAxis: ChartAxis{LogBase: 10.5}}}, + {cell: "A25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[2], Height: &dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1.9 scaling"}, YAxis: ChartAxis{LogBase: 1.9}}}, + {cell: "F25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[2], Height: &dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 2 scaling"}, YAxis: ChartAxis{LogBase: 2}}}, + {cell: "K25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[2], Height: &dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000.1 scaling"}, YAxis: ChartAxis{LogBase: 1000.1}}}, + {cell: "P25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[2], Height: &dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000 scaling"}, YAxis: ChartAxis{LogBase: 1000}}}, + } { + // Add two chart, one without and one with log scaling + assert.NoError(t, f.AddChart(sheet1, c.cell, c.opts)) + } // Export XLSX file for human confirmation assert.NoError(t, f.SaveAs(filepath.Join("test", "TestChartWithLogarithmicBase10.xlsx"))) diff --git a/col_test.go b/col_test.go index 4c5961f6e6..4debab0f8e 100644 --- a/col_test.go +++ b/col_test.go @@ -151,7 +151,6 @@ func TestGetColsError(t *testing.T) { func TestColsRows(t *testing.T) { f := NewFile() - f.NewSheet("Sheet1") _, err := f.Cols("Sheet1") assert.NoError(t, err) @@ -231,7 +230,8 @@ func TestColumnVisibility(t *testing.T) { // Test set column visible with invalid sheet name assert.EqualError(t, f.SetColVisible("Sheet:1", "A", false), ErrSheetNameInvalid.Error()) - f.NewSheet("Sheet3") + _, err = f.NewSheet("Sheet3") + assert.NoError(t, err) assert.NoError(t, f.SetColVisible("Sheet3", "E", false)) assert.EqualError(t, f.SetColVisible("Sheet1", "A:-1", true), newInvalidColumnNameError("-1").Error()) assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN does not exist") @@ -253,7 +253,8 @@ func TestOutlineLevel(t *testing.T) { assert.Equal(t, uint8(0), level) assert.NoError(t, err) - f.NewSheet("Sheet2") + _, err = f.NewSheet("Sheet2") + assert.NoError(t, err) assert.NoError(t, f.SetColOutlineLevel("Sheet1", "D", 4)) level, err = f.GetColOutlineLevel("Sheet1", "D") @@ -318,7 +319,8 @@ func TestOutlineLevel(t *testing.T) { func TestSetColStyle(t *testing.T) { f := NewFile() assert.NoError(t, f.SetCellValue("Sheet1", "B2", "Hello")) - styleID, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#94d3a2"],"pattern":1}}`) + + styleID, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"#94d3a2"}, Pattern: 1}}) assert.NoError(t, err) // Test set column style on not exists worksheet assert.EqualError(t, f.SetColStyle("SheetN", "E", styleID), "sheet SheetN does not exist") @@ -410,7 +412,7 @@ func TestInsertCols(t *testing.T) { assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.NoError(t, f.MergeCell(sheet1, "A1", "C3")) - assert.NoError(t, f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`)) + assert.NoError(t, f.AutoFilter(sheet1, "A2:B2", &AutoFilterOptions{Column: "B", Expression: "x != blanks"})) assert.NoError(t, f.InsertCols(sheet1, "A", 1)) // Test insert column with illegal cell reference diff --git a/datavalidation_test.go b/datavalidation_test.go index c307d20187..7260b1a74e 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -51,7 +51,8 @@ func TestDataValidation(t *testing.T) { assert.NoError(t, f.SaveAs(resultFile)) - f.NewSheet("Sheet2") + _, err = f.NewSheet("Sheet2") + assert.NoError(t, err) assert.NoError(t, f.SetSheetRow("Sheet2", "A2", &[]interface{}{"B2", 1})) assert.NoError(t, f.SetSheetRow("Sheet2", "A3", &[]interface{}{"B3", 3})) dvRange = NewDataValidation(true) diff --git a/docProps.go b/docProps.go index ebe929b03d..0531d4c9e9 100644 --- a/docProps.go +++ b/docProps.go @@ -147,6 +147,13 @@ func (f *File) GetAppProps() (ret *AppProperties, err error) { // Category | A categorization of the content of this package. // | // Version | The version number. This value is set by the user or by the application. +// | +// Modified | The created time of the content of the resource which +// | represent in ISO 8601 UTC format, for example "2019-06-04T22:00:10Z". +// | +// Modified | The modified time of the content of the resource which +// | represent in ISO 8601 UTC format, for example "2019-06-04T22:00:10Z". +// | // // For example: // diff --git a/docProps_test.go b/docProps_test.go index 64b690ce7c..da16a0456f 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -58,7 +58,7 @@ func TestGetAppProps(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.Close()) - // Test get application properties with unsupported charset. + // Test get application properties with unsupported charset f = NewFile() f.Pkg.Store(defaultXMLPathDocPropsApp, MacintoshCyrillicCharset) _, err = f.GetAppProps() @@ -110,7 +110,7 @@ func TestGetDocProps(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.Close()) - // Test get workbook properties with unsupported charset. + // Test get workbook properties with unsupported charset f = NewFile() f.Pkg.Store(defaultXMLPathDocPropsCore, MacintoshCyrillicCharset) _, err = f.GetDocProps() diff --git a/drawing.go b/drawing.go index 08b7aac767..de5a1d1c48 100644 --- a/drawing.go +++ b/drawing.go @@ -55,7 +55,7 @@ func (f *File) prepareChartSheetDrawing(cs *xlsxChartsheet, drawingID int, sheet // addChart provides a function to create chart as xl/charts/chart%d.xml by // given format sets. -func (f *File) addChart(opts *chartOptions, comboCharts []*chartOptions) { +func (f *File) addChart(opts *Chart, comboCharts []*Chart) { count := f.countCharts() xlsxChartSpace := xlsxChartSpace{ XMLNSa: NameSpaceDrawingML.Value, @@ -139,7 +139,7 @@ func (f *File) addChart(opts *chartOptions, comboCharts []*chartOptions) { }, PlotArea: &cPlotArea{}, Legend: &cLegend{ - LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[opts.Legend.Position])}, + LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[*opts.Legend.Position])}, Overlay: &attrValBool{Val: boolPtr(false)}, }, @@ -180,7 +180,7 @@ func (f *File) addChart(opts *chartOptions, comboCharts []*chartOptions) { }, }, } - plotAreaFunc := map[string]func(*chartOptions) *cPlotArea{ + plotAreaFunc := map[string]func(*Chart) *cPlotArea{ Area: f.drawBaseChart, AreaStacked: f.drawBaseChart, AreaPercentStacked: f.drawBaseChart, @@ -264,7 +264,7 @@ func (f *File) addChart(opts *chartOptions, comboCharts []*chartOptions) { // drawBaseChart provides a function to draw the c:plotArea element for bar, // and column series charts by given format sets. -func (f *File) drawBaseChart(opts *chartOptions) *cPlotArea { +func (f *File) drawBaseChart(opts *Chart) *cPlotArea { c := cCharts{ BarDir: &attrValString{ Val: stringPtr("col"), @@ -273,7 +273,7 @@ func (f *File) drawBaseChart(opts *chartOptions) *cPlotArea { Val: stringPtr("clustered"), }, VaryColors: &attrValBool{ - Val: boolPtr(opts.VaryColors), + Val: opts.VaryColors, }, Ser: f.drawChartSeries(opts), Shape: f.drawChartShape(opts), @@ -513,7 +513,7 @@ func (f *File) drawBaseChart(opts *chartOptions) *cPlotArea { // drawDoughnutChart provides a function to draw the c:plotArea element for // doughnut chart by given format sets. -func (f *File) drawDoughnutChart(opts *chartOptions) *cPlotArea { +func (f *File) drawDoughnutChart(opts *Chart) *cPlotArea { holeSize := 75 if opts.HoleSize > 0 && opts.HoleSize <= 90 { holeSize = opts.HoleSize @@ -522,7 +522,7 @@ func (f *File) drawDoughnutChart(opts *chartOptions) *cPlotArea { return &cPlotArea{ DoughnutChart: &cCharts{ VaryColors: &attrValBool{ - Val: boolPtr(opts.VaryColors), + Val: opts.VaryColors, }, Ser: f.drawChartSeries(opts), HoleSize: &attrValInt{Val: intPtr(holeSize)}, @@ -532,7 +532,7 @@ func (f *File) drawDoughnutChart(opts *chartOptions) *cPlotArea { // drawLineChart provides a function to draw the c:plotArea element for line // chart by given format sets. -func (f *File) drawLineChart(opts *chartOptions) *cPlotArea { +func (f *File) drawLineChart(opts *Chart) *cPlotArea { return &cPlotArea{ LineChart: &cCharts{ Grouping: &attrValString{ @@ -555,7 +555,7 @@ func (f *File) drawLineChart(opts *chartOptions) *cPlotArea { // drawLine3DChart provides a function to draw the c:plotArea element for line // chart by given format sets. -func (f *File) drawLine3DChart(opts *chartOptions) *cPlotArea { +func (f *File) drawLine3DChart(opts *Chart) *cPlotArea { return &cPlotArea{ Line3DChart: &cCharts{ Grouping: &attrValString{ @@ -578,11 +578,11 @@ func (f *File) drawLine3DChart(opts *chartOptions) *cPlotArea { // drawPieChart provides a function to draw the c:plotArea element for pie // chart by given format sets. -func (f *File) drawPieChart(opts *chartOptions) *cPlotArea { +func (f *File) drawPieChart(opts *Chart) *cPlotArea { return &cPlotArea{ PieChart: &cCharts{ VaryColors: &attrValBool{ - Val: boolPtr(opts.VaryColors), + Val: opts.VaryColors, }, Ser: f.drawChartSeries(opts), }, @@ -591,11 +591,11 @@ func (f *File) drawPieChart(opts *chartOptions) *cPlotArea { // drawPie3DChart provides a function to draw the c:plotArea element for 3D // pie chart by given format sets. -func (f *File) drawPie3DChart(opts *chartOptions) *cPlotArea { +func (f *File) drawPie3DChart(opts *Chart) *cPlotArea { return &cPlotArea{ Pie3DChart: &cCharts{ VaryColors: &attrValBool{ - Val: boolPtr(opts.VaryColors), + Val: opts.VaryColors, }, Ser: f.drawChartSeries(opts), }, @@ -604,14 +604,14 @@ func (f *File) drawPie3DChart(opts *chartOptions) *cPlotArea { // drawPieOfPieChart provides a function to draw the c:plotArea element for // pie chart by given format sets. -func (f *File) drawPieOfPieChart(opts *chartOptions) *cPlotArea { +func (f *File) drawPieOfPieChart(opts *Chart) *cPlotArea { return &cPlotArea{ OfPieChart: &cCharts{ OfPieType: &attrValString{ Val: stringPtr("pie"), }, VaryColors: &attrValBool{ - Val: boolPtr(opts.VaryColors), + Val: opts.VaryColors, }, Ser: f.drawChartSeries(opts), SerLines: &attrValString{}, @@ -621,14 +621,14 @@ func (f *File) drawPieOfPieChart(opts *chartOptions) *cPlotArea { // drawBarOfPieChart provides a function to draw the c:plotArea element for // pie chart by given format sets. -func (f *File) drawBarOfPieChart(opts *chartOptions) *cPlotArea { +func (f *File) drawBarOfPieChart(opts *Chart) *cPlotArea { return &cPlotArea{ OfPieChart: &cCharts{ OfPieType: &attrValString{ Val: stringPtr("bar"), }, VaryColors: &attrValBool{ - Val: boolPtr(opts.VaryColors), + Val: opts.VaryColors, }, Ser: f.drawChartSeries(opts), SerLines: &attrValString{}, @@ -638,7 +638,7 @@ func (f *File) drawBarOfPieChart(opts *chartOptions) *cPlotArea { // drawRadarChart provides a function to draw the c:plotArea element for radar // chart by given format sets. -func (f *File) drawRadarChart(opts *chartOptions) *cPlotArea { +func (f *File) drawRadarChart(opts *Chart) *cPlotArea { return &cPlotArea{ RadarChart: &cCharts{ RadarStyle: &attrValString{ @@ -661,7 +661,7 @@ func (f *File) drawRadarChart(opts *chartOptions) *cPlotArea { // drawScatterChart provides a function to draw the c:plotArea element for // scatter chart by given format sets. -func (f *File) drawScatterChart(opts *chartOptions) *cPlotArea { +func (f *File) drawScatterChart(opts *Chart) *cPlotArea { return &cPlotArea{ ScatterChart: &cCharts{ ScatterStyle: &attrValString{ @@ -684,7 +684,7 @@ func (f *File) drawScatterChart(opts *chartOptions) *cPlotArea { // drawSurface3DChart provides a function to draw the c:surface3DChart element by // given format sets. -func (f *File) drawSurface3DChart(opts *chartOptions) *cPlotArea { +func (f *File) drawSurface3DChart(opts *Chart) *cPlotArea { plotArea := &cPlotArea{ Surface3DChart: &cCharts{ Ser: f.drawChartSeries(opts), @@ -706,7 +706,7 @@ func (f *File) drawSurface3DChart(opts *chartOptions) *cPlotArea { // drawSurfaceChart provides a function to draw the c:surfaceChart element by // given format sets. -func (f *File) drawSurfaceChart(opts *chartOptions) *cPlotArea { +func (f *File) drawSurfaceChart(opts *Chart) *cPlotArea { plotArea := &cPlotArea{ SurfaceChart: &cCharts{ Ser: f.drawChartSeries(opts), @@ -728,7 +728,7 @@ func (f *File) drawSurfaceChart(opts *chartOptions) *cPlotArea { // drawChartShape provides a function to draw the c:shape element by given // format sets. -func (f *File) drawChartShape(opts *chartOptions) *attrValString { +func (f *File) drawChartShape(opts *Chart) *attrValString { shapes := map[string]string{ Bar3DConeClustered: "cone", Bar3DConeStacked: "cone", @@ -760,7 +760,7 @@ func (f *File) drawChartShape(opts *chartOptions) *attrValString { // drawChartSeries provides a function to draw the c:ser element by given // format sets. -func (f *File) drawChartSeries(opts *chartOptions) *[]cSer { +func (f *File) drawChartSeries(opts *Chart) *[]cSer { var ser []cSer for k := range opts.Series { ser = append(ser, cSer{ @@ -790,7 +790,7 @@ func (f *File) drawChartSeries(opts *chartOptions) *[]cSer { // drawChartSeriesSpPr provides a function to draw the c:spPr element by given // format sets. -func (f *File) drawChartSeriesSpPr(i int, opts *chartOptions) *cSpPr { +func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr { var srgbClr *attrValString var schemeClr *aSchemeClr @@ -823,7 +823,7 @@ func (f *File) drawChartSeriesSpPr(i int, opts *chartOptions) *cSpPr { // drawChartSeriesDPt provides a function to draw the c:dPt element by given // data index and format sets. -func (f *File) drawChartSeriesDPt(i int, opts *chartOptions) []*cDPt { +func (f *File) drawChartSeriesDPt(i int, opts *Chart) []*cDPt { dpt := []*cDPt{{ IDx: &attrValInt{Val: intPtr(i)}, Bubble3D: &attrValBool{Val: boolPtr(false)}, @@ -852,7 +852,7 @@ func (f *File) drawChartSeriesDPt(i int, opts *chartOptions) []*cDPt { // drawChartSeriesCat provides a function to draw the c:cat element by given // chart series and format sets. -func (f *File) drawChartSeriesCat(v chartSeriesOptions, opts *chartOptions) *cCat { +func (f *File) drawChartSeriesCat(v ChartSeries, opts *Chart) *cCat { cat := &cCat{ StrRef: &cStrRef{ F: v.Categories, @@ -867,7 +867,7 @@ func (f *File) drawChartSeriesCat(v chartSeriesOptions, opts *chartOptions) *cCa // drawChartSeriesVal provides a function to draw the c:val element by given // chart series and format sets. -func (f *File) drawChartSeriesVal(v chartSeriesOptions, opts *chartOptions) *cVal { +func (f *File) drawChartSeriesVal(v ChartSeries, opts *Chart) *cVal { val := &cVal{ NumRef: &cNumRef{ F: v.Values, @@ -882,7 +882,7 @@ func (f *File) drawChartSeriesVal(v chartSeriesOptions, opts *chartOptions) *cVa // drawChartSeriesMarker provides a function to draw the c:marker element by // given data index and format sets. -func (f *File) drawChartSeriesMarker(i int, opts *chartOptions) *cMarker { +func (f *File) drawChartSeriesMarker(i int, opts *Chart) *cMarker { defaultSymbol := map[string]*attrValString{Scatter: {Val: stringPtr("circle")}} marker := &cMarker{ Symbol: defaultSymbol[opts.Type], @@ -917,7 +917,7 @@ func (f *File) drawChartSeriesMarker(i int, opts *chartOptions) *cMarker { // drawChartSeriesXVal provides a function to draw the c:xVal element by given // chart series and format sets. -func (f *File) drawChartSeriesXVal(v chartSeriesOptions, opts *chartOptions) *cCat { +func (f *File) drawChartSeriesXVal(v ChartSeries, opts *Chart) *cCat { cat := &cCat{ StrRef: &cStrRef{ F: v.Categories, @@ -929,7 +929,7 @@ func (f *File) drawChartSeriesXVal(v chartSeriesOptions, opts *chartOptions) *cC // drawChartSeriesYVal provides a function to draw the c:yVal element by given // chart series and format sets. -func (f *File) drawChartSeriesYVal(v chartSeriesOptions, opts *chartOptions) *cVal { +func (f *File) drawChartSeriesYVal(v ChartSeries, opts *Chart) *cVal { val := &cVal{ NumRef: &cNumRef{ F: v.Values, @@ -941,7 +941,7 @@ func (f *File) drawChartSeriesYVal(v chartSeriesOptions, opts *chartOptions) *cV // drawCharSeriesBubbleSize provides a function to draw the c:bubbleSize // element by given chart series and format sets. -func (f *File) drawCharSeriesBubbleSize(v chartSeriesOptions, opts *chartOptions) *cVal { +func (f *File) drawCharSeriesBubbleSize(v ChartSeries, opts *Chart) *cVal { if _, ok := map[string]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok { return nil } @@ -954,7 +954,7 @@ func (f *File) drawCharSeriesBubbleSize(v chartSeriesOptions, opts *chartOptions // drawCharSeriesBubble3D provides a function to draw the c:bubble3D element // by given format sets. -func (f *File) drawCharSeriesBubble3D(opts *chartOptions) *attrValBool { +func (f *File) drawCharSeriesBubble3D(opts *Chart) *attrValBool { if _, ok := map[string]bool{Bubble3D: true}[opts.Type]; !ok { return nil } @@ -963,21 +963,21 @@ func (f *File) drawCharSeriesBubble3D(opts *chartOptions) *attrValBool { // drawChartDLbls provides a function to draw the c:dLbls element by given // format sets. -func (f *File) drawChartDLbls(opts *chartOptions) *cDLbls { +func (f *File) drawChartDLbls(opts *Chart) *cDLbls { return &cDLbls{ ShowLegendKey: &attrValBool{Val: boolPtr(opts.Legend.ShowLegendKey)}, - ShowVal: &attrValBool{Val: boolPtr(opts.Plotarea.ShowVal)}, - ShowCatName: &attrValBool{Val: boolPtr(opts.Plotarea.ShowCatName)}, - ShowSerName: &attrValBool{Val: boolPtr(opts.Plotarea.ShowSerName)}, - ShowBubbleSize: &attrValBool{Val: boolPtr(opts.Plotarea.ShowBubbleSize)}, - ShowPercent: &attrValBool{Val: boolPtr(opts.Plotarea.ShowPercent)}, - ShowLeaderLines: &attrValBool{Val: boolPtr(opts.Plotarea.ShowLeaderLines)}, + ShowVal: &attrValBool{Val: boolPtr(opts.PlotArea.ShowVal)}, + ShowCatName: &attrValBool{Val: boolPtr(opts.PlotArea.ShowCatName)}, + ShowSerName: &attrValBool{Val: boolPtr(opts.PlotArea.ShowSerName)}, + ShowBubbleSize: &attrValBool{Val: boolPtr(opts.PlotArea.ShowBubbleSize)}, + ShowPercent: &attrValBool{Val: boolPtr(opts.PlotArea.ShowPercent)}, + ShowLeaderLines: &attrValBool{Val: boolPtr(opts.PlotArea.ShowLeaderLines)}, } } // drawChartSeriesDLbls provides a function to draw the c:dLbls element by // given format sets. -func (f *File) drawChartSeriesDLbls(opts *chartOptions) *cDLbls { +func (f *File) drawChartSeriesDLbls(opts *Chart) *cDLbls { dLbls := f.drawChartDLbls(opts) chartSeriesDLbls := map[string]*cDLbls{ Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil, @@ -989,7 +989,7 @@ func (f *File) drawChartSeriesDLbls(opts *chartOptions) *cDLbls { } // drawPlotAreaCatAx provides a function to draw the c:catAx element. -func (f *File) drawPlotAreaCatAx(opts *chartOptions) []*cAxs { +func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { max := &attrValFloat{Val: opts.XAxis.Maximum} min := &attrValFloat{Val: opts.XAxis.Minimum} if opts.XAxis.Maximum == nil { @@ -1025,10 +1025,10 @@ func (f *File) drawPlotAreaCatAx(opts *chartOptions) []*cAxs { NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, }, } - if opts.XAxis.MajorGridlines { + if opts.XAxis.MajorGridLines { axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } - if opts.XAxis.MinorGridlines { + if opts.XAxis.MinorGridLines { axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } if opts.XAxis.TickLabelSkip != 0 { @@ -1038,7 +1038,7 @@ func (f *File) drawPlotAreaCatAx(opts *chartOptions) []*cAxs { } // drawPlotAreaValAx provides a function to draw the c:valAx element. -func (f *File) drawPlotAreaValAx(opts *chartOptions) []*cAxs { +func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { max := &attrValFloat{Val: opts.YAxis.Maximum} min := &attrValFloat{Val: opts.YAxis.Minimum} if opts.YAxis.Maximum == nil { @@ -1076,10 +1076,10 @@ func (f *File) drawPlotAreaValAx(opts *chartOptions) []*cAxs { CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, }, } - if opts.YAxis.MajorGridlines { + if opts.YAxis.MajorGridLines { axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } - if opts.YAxis.MinorGridlines { + if opts.YAxis.MinorGridLines { axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } if pos, ok := valTickLblPos[opts.Type]; ok { @@ -1092,7 +1092,7 @@ func (f *File) drawPlotAreaValAx(opts *chartOptions) []*cAxs { } // drawPlotAreaSerAx provides a function to draw the c:serAx element. -func (f *File) drawPlotAreaSerAx(opts *chartOptions) []*cAxs { +func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs { max := &attrValFloat{Val: opts.YAxis.Maximum} min := &attrValFloat{Val: opts.YAxis.Minimum} if opts.YAxis.Maximum == nil { @@ -1139,7 +1139,7 @@ func (f *File) drawPlotAreaSpPr() *cSpPr { } // drawPlotAreaTxPr provides a function to draw the c:txPr element. -func (f *File) drawPlotAreaTxPr(opts *chartAxisOptions) *cTxPr { +func (f *File) drawPlotAreaTxPr(opts *ChartAxis) *cTxPr { cTxPr := &cTxPr{ BodyPr: aBodyPr{ Rot: -60000000, @@ -1242,7 +1242,7 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int, error) { // addDrawingChart provides a function to add chart graphic frame by given // sheet, drawingXML, cell, width, height, relationship index and format sets. -func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, opts *pictureOptions) error { +func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, opts *PictureOptions) error { col, row, err := CellNameToCoordinates(cell) if err != nil { return err @@ -1250,8 +1250,8 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI colIdx := col - 1 rowIdx := row - 1 - width = int(float64(width) * opts.XScale) - height = int(float64(height) * opts.YScale) + width = int(float64(width) * *opts.XScale) + height = int(float64(height) * *opts.YScale) colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.OffsetX, opts.OffsetY, width, height) content, cNvPrID, err := f.drawingParser(drawingXML) if err != nil { @@ -1293,8 +1293,8 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI graphic, _ := xml.Marshal(graphicFrame) twoCellAnchor.GraphicFrame = string(graphic) twoCellAnchor.ClientData = &xdrClientData{ - FLocksWithSheet: opts.FLocksWithSheet, - FPrintsWithSheet: opts.FPrintsWithSheet, + FLocksWithSheet: *opts.Locked, + FPrintsWithSheet: *opts.PrintObject, } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) f.Drawings.Store(drawingXML, content) @@ -1304,7 +1304,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI // addSheetDrawingChart provides a function to add chart graphic frame for // chartsheet by given sheet, drawingXML, width, height, relationship index // and format sets. -func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *pictureOptions) error { +func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *PictureOptions) error { content, cNvPrID, err := f.drawingParser(drawingXML) if err != nil { return err @@ -1336,8 +1336,8 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *pictureOpt graphic, _ := xml.Marshal(graphicFrame) absoluteAnchor.GraphicFrame = string(graphic) absoluteAnchor.ClientData = &xdrClientData{ - FLocksWithSheet: opts.FLocksWithSheet, - FPrintsWithSheet: opts.FPrintsWithSheet, + FLocksWithSheet: *opts.Locked, + FPrintsWithSheet: *opts.PrintObject, } content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor) f.Drawings.Store(drawingXML, content) diff --git a/drawing_test.go b/drawing_test.go index 5c090eb96d..f0580dda1f 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -26,13 +26,13 @@ func TestDrawingParser(t *testing.T) { } f.Pkg.Store("charset", MacintoshCyrillicCharset) f.Pkg.Store("wsDr", []byte(xml.Header+``)) - // Test with one cell anchor. + // Test with one cell anchor _, _, err := f.drawingParser("wsDr") assert.NoError(t, err) - // Test with unsupported charset. + // Test with unsupported charset _, _, err = f.drawingParser("charset") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") - // Test with alternate content. + // Test with alternate content f.Drawings = sync.Map{} f.Pkg.Store("wsDr", []byte(xml.Header+``)) _, _, err = f.drawingParser("wsDr") diff --git a/excelize_test.go b/excelize_test.go index 673664c7cb..b9e1403d7c 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -71,9 +71,10 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, f.SetCellStr("Sheet2", "C11", "Knowns")) // Test max characters in a cell assert.NoError(t, f.SetCellStr("Sheet2", "D11", strings.Repeat("c", TotalCellChars+2))) - f.NewSheet(":\\/?*[]Maximum 31 characters allowed in sheet title.") + _, err = f.NewSheet(":\\/?*[]Maximum 31 characters allowed in sheet title.") + assert.EqualError(t, err, ErrSheetNameLength.Error()) // Test set worksheet name with illegal name - f.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title.") + assert.EqualError(t, f.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title."), ErrSheetNameLength.Error()) assert.EqualError(t, f.SetCellInt("Sheet3", "A23", 10), "sheet Sheet3 does not exist") assert.EqualError(t, f.SetCellStr("Sheet3", "b230", "10"), "sheet Sheet3 does not exist") assert.EqualError(t, f.SetCellStr("Sheet10", "b230", "10"), "sheet Sheet10 does not exist") @@ -102,13 +103,13 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, err) getSharedFormula(&xlsxWorksheet{}, 0, "") - // Test read cell value with given illegal rows number. + // Test read cell value with given illegal rows number _, err = f.GetCellValue("Sheet2", "a-1") assert.EqualError(t, err, newCellNameToCoordinatesError("A-1", newInvalidCellNameError("A-1")).Error()) _, err = f.GetCellValue("Sheet2", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - // Test read cell value with given lowercase column number. + // Test read cell value with given lowercase column number _, err = f.GetCellValue("Sheet2", "a5") assert.NoError(t, err) _, err = f.GetCellValue("Sheet2", "C11") @@ -145,7 +146,7 @@ func TestOpenFile(t *testing.T) { assert.EqualError(t, f.SetCellHyperLink("SheetN", "A1", "Sheet1!A40", "Location"), "sheet SheetN does not exist") // Test boolean write - booltest := []struct { + boolTest := []struct { value bool raw bool expected string @@ -155,7 +156,7 @@ func TestOpenFile(t *testing.T) { {false, false, "FALSE"}, {true, false, "TRUE"}, } - for _, test := range booltest { + for _, test := range boolTest { assert.NoError(t, f.SetCellValue("Sheet2", "F16", test.value)) val, err := f.GetCellValue("Sheet2", "F16", Options{RawCellValue: test.raw}) assert.NoError(t, err) @@ -175,10 +176,12 @@ func TestOpenFile(t *testing.T) { // Test read cell value with given cell reference large than exists row _, err = f.GetCellValue("Sheet2", "E231") assert.NoError(t, err) - // Test get active worksheet of spreadsheet and get worksheet name of spreadsheet by given worksheet index + // Test get active worksheet of spreadsheet and get worksheet name of + // spreadsheet by given worksheet index f.GetSheetName(f.GetActiveSheetIndex()) // Test get worksheet index of spreadsheet by given worksheet name - f.GetSheetIndex("Sheet1") + _, err = f.GetSheetIndex("Sheet1") + assert.NoError(t, err) // Test get worksheet name of spreadsheet by given invalid worksheet index f.GetSheetName(4) // Test get worksheet map of workbook @@ -339,31 +342,23 @@ func TestBrokenFile(t *testing.T) { func TestNewFile(t *testing.T) { // Test create a spreadsheet file f := NewFile() - f.NewSheet("Sheet1") - f.NewSheet("XLSXSheet2") - f.NewSheet("XLSXSheet3") + _, err := f.NewSheet("Sheet1") + assert.NoError(t, err) + _, err = f.NewSheet("XLSXSheet2") + assert.NoError(t, err) + _, err = f.NewSheet("XLSXSheet3") + assert.NoError(t, err) assert.NoError(t, f.SetCellInt("XLSXSheet2", "A23", 56)) assert.NoError(t, f.SetCellStr("Sheet1", "B20", "42")) f.SetActiveSheet(0) // Test add picture to sheet with scaling and positioning - err := f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), - `{"x_scale": 0.5, "y_scale": 0.5, "positioning": "absolute"}`) - if !assert.NoError(t, err) { - t.FailNow() - } + scale := 0.5 + assert.NoError(t, f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), + &PictureOptions{XScale: &scale, YScale: &scale, Positioning: "absolute"})) // Test add picture to worksheet without options - err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), "") - if !assert.NoError(t, err) { - t.FailNow() - } - - // Test add picture to worksheet with invalid options - err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), `{`) - if !assert.Error(t, err) { - t.FailNow() - } + assert.NoError(t, f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), nil)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewFile.xlsx"))) assert.NoError(t, f.Save()) @@ -385,7 +380,7 @@ func TestSetCellHyperLink(t *testing.T) { assert.NoError(t, f.SetCellHyperLink("Sheet1", "B19", "https://github.com/xuri/excelize", "External")) // Test add first hyperlink in a work sheet assert.NoError(t, f.SetCellHyperLink("Sheet2", "C1", "https://github.com/xuri/excelize", "External")) - // Test add Location hyperlink in a work sheet. + // Test add Location hyperlink in a work sheet assert.NoError(t, f.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location")) // Test add Location hyperlink with display & tooltip in a work sheet display, tooltip := "Display value", "Hover text" @@ -429,9 +424,7 @@ func TestSetCellHyperLink(t *testing.T) { func TestGetCellHyperLink(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) _, _, err = f.GetCellHyperLink("Sheet1", "") assert.EqualError(t, err, `invalid cell name ""`) @@ -513,9 +506,9 @@ func TestSetSheetBackgroundErrors(t *testing.T) { assert.EqualError(t, f.SetSheetBackground("Sheet1", filepath.Join("test", "images", "background.jpg")), "XML syntax error on line 1: invalid UTF-8") } -// 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. +// 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) @@ -620,15 +613,11 @@ func TestWriteArrayFormula(t *testing.T) { func TestSetCellStyleAlignment(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) var style int - style, err = f.NewStyle(`{"alignment":{"horizontal":"center","ident":1,"justify_last_line":true,"reading_order":0,"relative_indent":1,"shrink_to_fit":true,"text_rotation":45,"vertical":"top","wrap_text":true}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{Alignment: &Alignment{Horizontal: "center", Indent: 1, JustifyLastLine: true, ReadingOrder: 0, RelativeIndent: 1, ShrinkToFit: true, TextRotation: 45, Vertical: "top", WrapText: true}}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A22", "A22", style)) @@ -651,13 +640,11 @@ func TestSetCellStyleAlignment(t *testing.T) { func TestSetCellStyleBorder(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) var style int - // Test set border on overlapping range with vertical variants shading styles gradient fill. + // Test set border on overlapping range with vertical variants shading styles gradient fill style, err = f.NewStyle(&Style{ Border: []Border{ {Type: "left", Color: "0000FF", Style: 3}, @@ -668,24 +655,18 @@ func TestSetCellStyleBorder(t *testing.T) { {Type: "diagonalUp", Color: "A020F0", Style: 8}, }, }) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "J21", "L25", style)) - style, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":2},{"type":"top","color":"00FF00","style":3},{"type":"bottom","color":"FFFF00","style":4},{"type":"right","color":"FF0000","style":5},{"type":"diagonalDown","color":"A020F0","style":6},{"type":"diagonalUp","color":"A020F0","style":7}],"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":1}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{Border: []Border{{Type: "left", Color: "0000FF", Style: 2}, {Type: "top", Color: "00FF00", Style: 3}, {Type: "bottom", Color: "FFFF00", Style: 4}, {Type: "right", Color: "FF0000", Style: 5}, {Type: "diagonalDown", Color: "A020F0", Style: 6}, {Type: "diagonalUp", Color: "A020F0", Style: 7}}, Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 1}}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "M28", "K24", style)) - style, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":2},{"type":"top","color":"00FF00","style":3},{"type":"bottom","color":"FFFF00","style":4},{"type":"right","color":"FF0000","style":5},{"type":"diagonalDown","color":"A020F0","style":6},{"type":"diagonalUp","color":"A020F0","style":7}],"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":4}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{Border: []Border{{Type: "left", Color: "0000FF", Style: 2}, {Type: "top", Color: "00FF00", Style: 3}, {Type: "bottom", Color: "FFFF00", Style: 4}, {Type: "right", Color: "FF0000", Style: 5}, {Type: "diagonalDown", Color: "A020F0", Style: 6}, {Type: "diagonalUp", Color: "A020F0", Style: 7}}, Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 4}}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "M28", "K24", style)) - // Test set border and solid style pattern fill for a single cell. + // Test set border and solid style pattern fill for a single cell style, err = f.NewStyle(&Style{ Border: []Border{ { @@ -725,9 +706,7 @@ func TestSetCellStyleBorder(t *testing.T) { Pattern: 1, }, }) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "O22", "O22", style)) @@ -736,30 +715,18 @@ func TestSetCellStyleBorder(t *testing.T) { func TestSetCellStyleBorderErrors(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - // Set border with invalid style parameter. - _, err = f.NewStyle("") - if !assert.EqualError(t, err, "unexpected end of JSON input") { - t.FailNow() - } + assert.NoError(t, err) - // Set border with invalid style index number. - _, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":-1},{"type":"top","color":"00FF00","style":14},{"type":"bottom","color":"FFFF00","style":5},{"type":"right","color":"FF0000","style":6},{"type":"diagonalDown","color":"A020F0","style":9},{"type":"diagonalUp","color":"A020F0","style":8}]}`) - if !assert.NoError(t, err) { - t.FailNow() - } + // Set border with invalid style index number + _, err = f.NewStyle(&Style{Border: []Border{{Type: "left", Color: "0000FF", Style: -1}, {Type: "top", Color: "00FF00", Style: 14}, {Type: "bottom", Color: "FFFF00", Style: 5}, {Type: "right", Color: "FF0000", Style: 6}, {Type: "diagonalDown", Color: "A020F0", Style: 9}, {Type: "diagonalUp", Color: "A020F0", Style: 8}}}) + assert.NoError(t, err) } func TestSetCellStyleNumberFormat(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) - // Test only set fill and number format for a cell. + // Test only set fill and number format for a cell col := []string{"L", "M", "N", "O", "P"} data := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49} value := []string{"37947.7500001", "-37947.7500001", "0.007", "2.1", "String"} @@ -781,7 +748,7 @@ func TestSetCellStyleNumberFormat(t *testing.T) { } else { assert.NoError(t, f.SetCellValue("Sheet2", c, val)) } - style, err := f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":5},"number_format": ` + strconv.Itoa(d) + `}`) + style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 5}, NumFmt: d}) if !assert.NoError(t, err) { t.FailNow() } @@ -792,10 +759,8 @@ func TestSetCellStyleNumberFormat(t *testing.T) { } } var style int - style, err = f.NewStyle(`{"number_format":-1}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{NumFmt: -1}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet2", "L33", "L33", style)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleNumberFormat.xlsx"))) @@ -804,23 +769,17 @@ func TestSetCellStyleNumberFormat(t *testing.T) { func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { t.Run("TestBook3", func(t *testing.T) { f, err := prepareTestBook3() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 56)) assert.NoError(t, f.SetCellValue("Sheet1", "A2", -32.3)) var style int - style, err = f.NewStyle(`{"number_format": 188, "decimal_places": -1}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: -1}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) - style, err = f.NewStyle(`{"number_format": 188, "decimal_places": 31, "negred": true}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: 31, NegRed: true}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) @@ -829,34 +788,24 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { t.Run("TestBook4", func(t *testing.T) { f, err := prepareTestBook4() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5)) assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5)) - _, err = f.NewStyle(`{"number_format": 26, "lang": "zh-tw"}`) - if !assert.NoError(t, err) { - t.FailNow() - } + _, err = f.NewStyle(&Style{NumFmt: 26, Lang: "zh-tw"}) + assert.NoError(t, err) - style, err := f.NewStyle(`{"number_format": 27}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err := f.NewStyle(&Style{NumFmt: 27}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) - style, err = f.NewStyle(`{"number_format": 31, "lang": "ko-kr"}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{NumFmt: 31, Lang: "ko-kr"}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) - style, err = f.NewStyle(`{"number_format": 71, "lang": "th-th"}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{NumFmt: 71, Lang: "th-th"}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleCurrencyNumberFormat.TestBook4.xlsx"))) @@ -867,42 +816,40 @@ func TestSetCellStyleCustomNumberFormat(t *testing.T) { f := NewFile() assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5)) assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5)) - style, err := f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) + customNumFmt := "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@" + style, err := f.NewStyle(&Style{CustomNumFmt: &customNumFmt}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) - style, err = f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@","font":{"color":"#9A0511"}}`) + style, err = f.NewStyle(&Style{CustomNumFmt: &customNumFmt, Font: &Font{Color: "#9A0511"}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) - _, err = f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yy;@"}`) + customNumFmt = "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yy;@" + _, err = f.NewStyle(&Style{CustomNumFmt: &customNumFmt}) assert.NoError(t, err) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleCustomNumberFormat.xlsx"))) } func TestSetCellStyleFill(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) var style int - // Test set fill for cell with invalid parameter. - style, err = f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":6}}`) + // Test set fill for cell with invalid parameter + style, err = f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 6}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) - style, err = f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF"],"shading":1}}`) + style, err = f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF"}, Shading: 1}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) - style, err = f.NewStyle(`{"fill":{"type":"pattern","color":[],"pattern":1}}`) + style, err = f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{}, Shading: 1}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) - style, err = f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":19}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"E0EBF5"}, Pattern: 19}}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleFill.xlsx"))) @@ -910,43 +857,31 @@ func TestSetCellStyleFill(t *testing.T) { func TestSetCellStyleFont(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) var style int - style, err = f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{Font: &Font{Bold: true, Italic: true, Family: "Times New Roman", Size: 36, Color: "#777777", Underline: "single"}}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet2", "A1", "A1", style)) - style, err = f.NewStyle(`{"font":{"italic":true,"underline":"double"}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{Font: &Font{Italic: true, Underline: "double"}}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet2", "A2", "A2", style)) - style, err = f.NewStyle(`{"font":{"bold":true}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{Font: &Font{Bold: true}}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet2", "A3", "A3", style)) - style, err = f.NewStyle(`{"font":{"bold":true,"family":"","size":0,"color":"","underline":""}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{Font: &Font{Bold: true, Family: "", Size: 0, Color: "", Underline: ""}}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet2", "A4", "A4", style)) - style, err = f.NewStyle(`{"font":{"color":"#777777","strike":true}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{Font: &Font{Color: "#777777", Strike: true}}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet2", "A5", "A5", style)) @@ -955,40 +890,30 @@ func TestSetCellStyleFont(t *testing.T) { func TestSetCellStyleProtection(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) var style int - style, err = f.NewStyle(`{"protection":{"hidden":true, "locked":true}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + style, err = f.NewStyle(&Style{Protection: &Protection{Hidden: true, Locked: true}}) + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet2", "A6", "A6", style)) err = f.SaveAs(filepath.Join("test", "TestSetCellStyleProtection.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) } func TestSetDeleteSheet(t *testing.T) { t.Run("TestBook3", func(t *testing.T) { f, err := prepareTestBook3() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) - f.DeleteSheet("XLSXSheet3") + assert.NoError(t, f.DeleteSheet("XLSXSheet3")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook3.xlsx"))) }) t.Run("TestBook4", func(t *testing.T) { f, err := prepareTestBook4() - if !assert.NoError(t, err) { - t.FailNow() - } - f.DeleteSheet("Sheet1") + assert.NoError(t, err) + assert.NoError(t, f.DeleteSheet("Sheet1")) assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A1", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx"))) }) @@ -996,11 +921,10 @@ func TestSetDeleteSheet(t *testing.T) { func TestSheetVisibility(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetSheetVisible("Sheet2", false)) + assert.NoError(t, f.SetSheetVisible("Sheet2", false, true)) assert.NoError(t, f.SetSheetVisible("Sheet1", false)) assert.NoError(t, f.SetSheetVisible("Sheet1", true)) visible, err := f.GetSheetVisible("Sheet1") @@ -1058,123 +982,231 @@ func TestConditionalFormat(t *testing.T) { var format1, format2, format3, format4 int var err error - // Rose format for bad conditional. - format1, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + // Rose format for bad conditional + format1, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "#9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"#FEC7CE"}, Pattern: 1}}) + assert.NoError(t, err) - // Light yellow format for neutral conditional. - format2, err = f.NewConditionalStyle(`{"fill":{"type":"pattern","color":["#FEEAA0"],"pattern":1}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + // Light yellow format for neutral conditional + format2, err = f.NewConditionalStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"#FEEAA0"}, Pattern: 1}}) + assert.NoError(t, err) - // Light green format for good conditional. - format3, err = f.NewConditionalStyle(`{"font":{"color":"#09600B"},"fill":{"type":"pattern","color":["#C7EECF"],"pattern":1}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + // Light green format for good conditional + format3, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "#09600B"}, Fill: Fill{Type: "pattern", Color: []string{"#C7EECF"}, Pattern: 1}}) + assert.NoError(t, err) - // conditional style with align and left border. - format4, err = f.NewConditionalStyle(`{"alignment":{"wrap_text":true},"border":[{"type":"left","color":"#000000","style":1}]}`) - if !assert.NoError(t, err) { - t.FailNow() - } + // conditional style with align and left border + format4, err = f.NewConditionalStyle(&Style{Alignment: &Alignment{WrapText: true}, Border: []Border{{Type: "left", Color: "#000000", Style: 1}}}) + assert.NoError(t, err) // Color scales: 2 color - assert.NoError(t, f.SetConditionalFormat(sheet1, "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "A1:A10", + []ConditionalFormatOptions{ + { + Type: "2_color_scale", + Criteria: "=", + MinType: "min", + MaxType: "max", + MinColor: "#F8696B", + MaxColor: "#63BE7B", + }, + }, + )) // Color scales: 3 color - assert.NoError(t, f.SetConditionalFormat(sheet1, "B1:B10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "B1:B10", + []ConditionalFormatOptions{ + { + Type: "3_color_scale", + Criteria: "=", + MinType: "min", + MidType: "percentile", + MaxType: "max", + MinColor: "#F8696B", + MidColor: "#FFEB84", + MaxColor: "#63BE7B", + }, + }, + )) // Highlight cells rules: between... - assert.NoError(t, f.SetConditionalFormat(sheet1, "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1))) + assert.NoError(t, f.SetConditionalFormat(sheet1, "C1:C10", + []ConditionalFormatOptions{ + { + Type: "cell", + Criteria: "between", + Format: format1, + Minimum: "6", + Maximum: "8", + }, + }, + )) // Highlight cells rules: Greater Than... - assert.NoError(t, f.SetConditionalFormat(sheet1, "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3))) + assert.NoError(t, f.SetConditionalFormat(sheet1, "D1:D10", + []ConditionalFormatOptions{ + { + Type: "cell", + Criteria: ">", + Format: format3, + Value: "6", + }, + }, + )) // Highlight cells rules: Equal To... - assert.NoError(t, f.SetConditionalFormat(sheet1, "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3))) + assert.NoError(t, f.SetConditionalFormat(sheet1, "E1:E10", + []ConditionalFormatOptions{ + { + Type: "top", + Criteria: "=", + Format: format3, + }, + }, + )) // Highlight cells rules: Not Equal To... - assert.NoError(t, f.SetConditionalFormat(sheet1, "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2))) + assert.NoError(t, f.SetConditionalFormat(sheet1, "F1:F10", + []ConditionalFormatOptions{ + { + Type: "unique", + Criteria: "=", + Format: format2, + }, + }, + )) // Highlight cells rules: Duplicate Values... - assert.NoError(t, f.SetConditionalFormat(sheet1, "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2))) + assert.NoError(t, f.SetConditionalFormat(sheet1, "G1:G10", + []ConditionalFormatOptions{ + { + Type: "duplicate", + Criteria: "=", + Format: format2, + }, + }, + )) // Top/Bottom rules: Top 10%. - assert.NoError(t, f.SetConditionalFormat(sheet1, "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1))) + assert.NoError(t, f.SetConditionalFormat(sheet1, "H1:H10", + []ConditionalFormatOptions{ + { + Type: "top", + Criteria: "=", + Format: format1, + Value: "6", + Percent: true, + }, + }, + )) // Top/Bottom rules: Above Average... - assert.NoError(t, f.SetConditionalFormat(sheet1, "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3))) + assert.NoError(t, f.SetConditionalFormat(sheet1, "I1:I10", + []ConditionalFormatOptions{ + { + Type: "average", + Criteria: "=", + Format: format3, + AboveAverage: true, + }, + }, + )) // Top/Bottom rules: Below Average... - assert.NoError(t, f.SetConditionalFormat(sheet1, "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1))) + assert.NoError(t, f.SetConditionalFormat(sheet1, "J1:J10", + []ConditionalFormatOptions{ + { + Type: "average", + Criteria: "=", + Format: format1, + AboveAverage: false, + }, + }, + )) // Data Bars: Gradient Fill - assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", + []ConditionalFormatOptions{ + { + Type: "data_bar", + Criteria: "=", + MinType: "min", + MaxType: "max", + BarColor: "#638EC6", + }, + }, + )) // Use a formula to determine which cells to format - assert.NoError(t, f.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1))) + assert.NoError(t, f.SetConditionalFormat(sheet1, "L1:L10", + []ConditionalFormatOptions{ + { + Type: "formula", + Criteria: "L2<3", + Format: format1, + }, + }, + )) // Alignment/Border cells rules - assert.NoError(t, f.SetConditionalFormat(sheet1, "M1:M10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"0"}]`, format4))) - - // Test set invalid format set in conditional format - assert.EqualError(t, f.SetConditionalFormat(sheet1, "L1:L10", ""), "unexpected end of JSON input") + assert.NoError(t, f.SetConditionalFormat(sheet1, "M1:M10", + []ConditionalFormatOptions{ + { + Type: "cell", + Criteria: ">", + Format: format4, + Value: "0", + }, + }, + )) // Test set conditional format on not exists worksheet - assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", "[]"), "sheet SheetN does not exist") + assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", nil), "sheet SheetN does not exist") // Test set conditional format with invalid sheet name - assert.EqualError(t, f.SetConditionalFormat("Sheet:1", "L1:L10", "[]"), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.SetConditionalFormat("Sheet:1", "L1:L10", nil), ErrSheetNameInvalid.Error()) err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) // Set conditional format with illegal valid type - assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", + []ConditionalFormatOptions{ + { + Type: "", + Criteria: "=", + MinType: "min", + MaxType: "max", + BarColor: "#638EC6", + }, + }, + )) // Set conditional format with illegal criteria type - assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", + []ConditionalFormatOptions{ + { + Type: "data_bar", + Criteria: "", + MinType: "min", + MaxType: "max", + BarColor: "#638EC6", + }, + }, + )) + // Test create conditional format with invalid custom number format + var exp string + _, err = f.NewConditionalStyle(&Style{CustomNumFmt: &exp}) + assert.EqualError(t, err, ErrCustomNumFmt.Error()) // Set conditional format with file without dxfs element should not return error f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) - _, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + _, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "#9A0511"}, Fill: Fill{Type: "", Color: []string{"#FEC7CE"}, Pattern: 1}}) + assert.NoError(t, err) assert.NoError(t, f.Close()) } -func TestConditionalFormatError(t *testing.T) { - f := NewFile() - sheet1 := f.GetSheetName(0) - - fillCells(f, sheet1, 10, 15) - - // Set conditional format with illegal JSON string should return error. - _, err := f.NewConditionalStyle("") - if !assert.EqualError(t, err, "unexpected end of JSON input") { - t.FailNow() - } -} - func TestSharedStrings(t *testing.T) { f, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) rows, err := f.GetRows("Sheet1") - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.Equal(t, "A", rows[0][0]) rows, err = f.GetRows("Sheet2") - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.Equal(t, "Test Weight (Kgs)", rows[0][0]) assert.NoError(t, f.Close()) } func TestSetSheetCol(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetSheetCol("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()})) @@ -1190,9 +1222,7 @@ func TestSetSheetCol(t *testing.T) { func TestSetSheetRow(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()})) @@ -1300,10 +1330,8 @@ func TestProtectSheet(t *testing.T) { func TestUnprotectSheet(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - // Test remove protection on not exists worksheet. + assert.NoError(t, err) + // Test remove protection on not exists worksheet assert.EqualError(t, f.UnprotectSheet("SheetN"), "sheet SheetN does not exist") assert.NoError(t, f.UnprotectSheet("Sheet1")) @@ -1343,7 +1371,7 @@ func TestProtectWorkbook(t *testing.T) { assert.Equal(t, 24, len(wb.WorkbookProtection.WorkbookSaltValue)) assert.Equal(t, 88, len(wb.WorkbookProtection.WorkbookHashValue)) assert.Equal(t, int(workbookProtectionSpinCount), wb.WorkbookProtection.WorkbookSpinCount) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestProtectWorkbook.xlsx"))) + // Test protect workbook with password exceeds the limit length assert.EqualError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{ AlgorithmName: "MD4", @@ -1354,13 +1382,15 @@ func TestProtectWorkbook(t *testing.T) { AlgorithmName: "RIPEMD-160", Password: "password", }), ErrUnsupportedHashAlgorithm.Error()) + // Test protect workbook with unsupported charset workbook + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.ProtectWorkbook(nil), "XML syntax error on line 1: invalid UTF-8") } func TestUnprotectWorkbook(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.UnprotectWorkbook()) assert.EqualError(t, f.UnprotectWorkbook("password"), ErrUnprotectWorkbook.Error()) @@ -1382,6 +1412,10 @@ func TestUnprotectWorkbook(t *testing.T) { assert.NoError(t, err) wb.WorkbookProtection.WorkbookSaltValue = "YWJjZA=====" assert.EqualError(t, f.UnprotectWorkbook("wrongPassword"), "illegal base64 data at input byte 8") + // Test remove workbook protection with unsupported charset workbook + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.UnprotectWorkbook(), "XML syntax error on line 1: invalid UTF-8") } func TestSetDefaultTimeStyle(t *testing.T) { @@ -1409,7 +1443,7 @@ func TestAddVBAProject(t *testing.T) { } func TestContentTypesReader(t *testing.T) { - // Test unsupported charset. + // Test unsupported charset f := NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) @@ -1418,7 +1452,7 @@ func TestContentTypesReader(t *testing.T) { } func TestWorkbookReader(t *testing.T) { - // Test unsupported charset. + // Test unsupported charset f := NewFile() f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) @@ -1427,7 +1461,7 @@ func TestWorkbookReader(t *testing.T) { } func TestWorkSheetReader(t *testing.T) { - // Test unsupported charset. + // Test unsupported charset f := NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) @@ -1435,7 +1469,7 @@ func TestWorkSheetReader(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.UpdateLinkedValue(), "XML syntax error on line 1: invalid UTF-8") - // Test on no checked worksheet. + // Test on no checked worksheet f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(``)) @@ -1445,7 +1479,7 @@ func TestWorkSheetReader(t *testing.T) { } func TestRelsReader(t *testing.T) { - // Test unsupported charset. + // Test unsupported charset f := NewFile() rels := defaultXMLPathWorkbookRels f.Relationships.Store(rels, nil) @@ -1463,7 +1497,7 @@ func TestDeleteSheetFromWorkbookRels(t *testing.T) { func TestUpdateLinkedValue(t *testing.T) { f := NewFile() - // Test update lined value with unsupported charset workbook. + // Test update lined value with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.UpdateLinkedValue(), "XML syntax error on line 1: invalid UTF-8") @@ -1482,16 +1516,21 @@ func prepareTestBook1() (*File, error) { return nil, err } - err = f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), - `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) - if err != nil { + if err = f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), + &PictureOptions{OffsetX: 140, OffsetY: 120, Hyperlink: "#Sheet2!D8", HyperlinkType: "Location"}); err != nil { return nil, err } - // Test add picture to worksheet with offset, external hyperlink and positioning. - err = f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.png"), - `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) - if err != nil { + // Test add picture to worksheet with offset, external hyperlink and positioning + if err := f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.png"), + &PictureOptions{ + OffsetX: 10, + OffsetY: 10, + Hyperlink: "https://github.com/xuri/excelize", + HyperlinkType: "External", + Positioning: "oneCell", + }, + ); err != nil { return nil, err } @@ -1500,7 +1539,7 @@ func prepareTestBook1() (*File, error) { return nil, err } - err = f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file) + err = f.AddPictureFromBytes("Sheet1", "Q1", "Excel Logo", ".jpg", file, nil) if err != nil { return nil, err } @@ -1510,9 +1549,12 @@ func prepareTestBook1() (*File, error) { func prepareTestBook3() (*File, error) { f := NewFile() - f.NewSheet("Sheet1") - f.NewSheet("XLSXSheet2") - f.NewSheet("XLSXSheet3") + if _, err := f.NewSheet("XLSXSheet2"); err != nil { + return nil, err + } + if _, err := f.NewSheet("XLSXSheet3"); err != nil { + return nil, err + } if err := f.SetCellInt("XLSXSheet2", "A23", 56); err != nil { return nil, err } @@ -1520,18 +1562,14 @@ func prepareTestBook3() (*File, error) { return nil, err } f.SetActiveSheet(0) - - err := f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), - `{"x_scale": 0.5, "y_scale": 0.5, "positioning": "absolute"}`) - if err != nil { + scale := 0.5 + if err := f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), + &PictureOptions{XScale: &scale, YScale: &scale, Positioning: "absolute"}); err != nil { return nil, err } - - err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), "") - if err != nil { + if err := f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), nil); err != nil { return nil, err } - return f, nil } diff --git a/lib.go b/lib.go index 27b5ab772b..d62b789b2b 100644 --- a/lib.go +++ b/lib.go @@ -313,15 +313,15 @@ func sortCoordinates(coordinates []int) error { // coordinatesToRangeRef provides a function to convert a pair of coordinates // to range reference. -func (f *File) coordinatesToRangeRef(coordinates []int) (string, error) { +func (f *File) coordinatesToRangeRef(coordinates []int, abs ...bool) (string, error) { if len(coordinates) != 4 { return "", ErrCoordinates } - firstCell, err := CoordinatesToCellName(coordinates[0], coordinates[1]) + firstCell, err := CoordinatesToCellName(coordinates[0], coordinates[1], abs...) if err != nil { return "", err } - lastCell, err := CoordinatesToCellName(coordinates[2], coordinates[3]) + lastCell, err := CoordinatesToCellName(coordinates[2], coordinates[3], abs...) if err != nil { return "", err } @@ -493,15 +493,6 @@ func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err return nil } -// fallbackOptions provides a method to convert format string to []byte and -// handle empty string. -func fallbackOptions(opts string) []byte { - if opts != "" { - return []byte(opts) - } - return []byte("{}") -} - // namespaceStrictToTransitional provides a method to convert Strict and // Transitional namespaces. func namespaceStrictToTransitional(content []byte) []byte { diff --git a/merge_test.go b/merge_test.go index 40055c9ccd..9bef612cb7 100644 --- a/merge_test.go +++ b/merge_test.go @@ -37,7 +37,8 @@ func TestMergeCell(t *testing.T) { assert.Equal(t, "SUM(Sheet1!B19,Sheet1!C19)", value) assert.NoError(t, err) - f.NewSheet("Sheet3") + _, err = f.NewSheet("Sheet3") + assert.NoError(t, err) assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13")) assert.NoError(t, f.MergeCell("Sheet3", "G10", "K12")) diff --git a/picture.go b/picture.go index 045e2af057..72d3f7d983 100644 --- a/picture.go +++ b/picture.go @@ -13,7 +13,6 @@ package excelize import ( "bytes" - "encoding/json" "encoding/xml" "image" "io" @@ -26,14 +25,28 @@ import ( // parsePictureOptions provides a function to parse the format settings of // the picture with default value. -func parsePictureOptions(opts string) (*pictureOptions, error) { - format := pictureOptions{ - FPrintsWithSheet: true, - XScale: 1, - YScale: 1, - } - err := json.Unmarshal(fallbackOptions(opts), &format) - return &format, err +func parsePictureOptions(opts *PictureOptions) *PictureOptions { + if opts == nil { + return &PictureOptions{ + PrintObject: boolPtr(true), + Locked: boolPtr(false), + XScale: float64Ptr(defaultPictureScale), + YScale: float64Ptr(defaultPictureScale), + } + } + if opts.PrintObject == nil { + opts.PrintObject = boolPtr(true) + } + if opts.Locked == nil { + opts.Locked = boolPtr(false) + } + if opts.XScale == nil { + opts.XScale = float64Ptr(defaultPictureScale) + } + if opts.YScale == nil { + opts.YScale = float64Ptr(defaultPictureScale) + } + return opts } // AddPicture provides the method to add picture in a sheet by given picture @@ -44,6 +57,7 @@ func parsePictureOptions(opts string) (*pictureOptions, error) { // package main // // import ( +// "fmt" // _ "image/gif" // _ "image/jpeg" // _ "image/png" @@ -54,15 +68,33 @@ func parsePictureOptions(opts string) (*pictureOptions, error) { // func main() { // f := excelize.NewFile() // // Insert a picture. -// if err := f.AddPicture("Sheet1", "A2", "image.jpg", ""); err != nil { +// if err := f.AddPicture("Sheet1", "A2", "image.jpg", nil); err != nil { // fmt.Println(err) // } // // Insert a picture scaling in the cell with location hyperlink. -// if err := f.AddPicture("Sheet1", "D2", "image.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`); err != nil { +// enable, scale := true, 0.5 +// if err := f.AddPicture("Sheet1", "D2", "image.png", +// &excelize.PictureOptions{ +// XScale: &scale, +// YScale: &scale, +// Hyperlink: "#Sheet2!D8", +// HyperlinkType: "Location", +// }, +// ); err != nil { // fmt.Println(err) // } // // Insert a picture offset in the cell with external hyperlink, printing and positioning support. -// if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`); err != nil { +// if err := f.AddPicture("Sheet1", "H2", "image.gif", +// &excelize.PictureOptions{ +// PrintObject: &enable, +// LockAspectRatio: false, +// OffsetX: 15, +// OffsetY: 10, +// Hyperlink: "https://github.com/xuri/excelize", +// HyperlinkType: "External", +// Positioning: "oneCell", +// }, +// ); err != nil { // fmt.Println(err) // } // if err := f.SaveAs("Book1.xlsx"); err != nil { @@ -70,42 +102,42 @@ func parsePictureOptions(opts string) (*pictureOptions, error) { // } // } // -// The optional parameter "autofit" specifies if you make image size auto-fits the +// The optional parameter "Autofit" specifies if you make image size auto-fits the // cell, the default value of that is 'false'. // -// The optional parameter "hyperlink" specifies the hyperlink of the image. +// The optional parameter "Hyperlink" specifies the hyperlink of the image. // -// The optional parameter "hyperlink_type" defines two types of +// The optional parameter "HyperlinkType" defines two types of // hyperlink "External" for website or "Location" for moving to one of the // cells in this workbook. When the "hyperlink_type" is "Location", // coordinates need to start with "#". // -// The optional parameter "positioning" defines two types of the position of an +// The optional parameter "Positioning" defines two types of the position of an // image in an Excel spreadsheet, "oneCell" (Move but don't size with // cells) or "absolute" (Don't move or size with cells). If you don't set this // parameter, the default positioning is move and size with cells. // -// The optional parameter "print_obj" indicates whether the image is printed +// The optional parameter "PrintObject" indicates whether the image is printed // when the worksheet is printed, the default value of that is 'true'. // -// The optional parameter "lock_aspect_ratio" indicates whether lock aspect +// The optional parameter "LockAspectRatio" indicates whether lock aspect // ratio for the image, the default value of that is 'false'. // -// The optional parameter "locked" indicates whether lock the image. Locking +// The optional parameter "Locked" indicates whether lock the image. Locking // an object has no effect unless the sheet is protected. // -// The optional parameter "x_offset" specifies the horizontal offset of the +// The optional parameter "OffsetX" specifies the horizontal offset of the // image with the cell, the default value of that is 0. // -// The optional parameter "x_scale" specifies the horizontal scale of images, +// The optional parameter "XScale" specifies the horizontal scale of images, // the default value of that is 1.0 which presents 100%. // -// The optional parameter "y_offset" specifies the vertical offset of the +// The optional parameter "OffsetY" specifies the vertical offset of the // image with the cell, the default value of that is 0. // -// The optional parameter "y_scale" specifies the vertical scale of images, +// The optional parameter "YScale" specifies the vertical scale of images, // the default value of that is 1.0 which presents 100%. -func (f *File) AddPicture(sheet, cell, picture, format string) error { +func (f *File) AddPicture(sheet, cell, picture string, opts *PictureOptions) error { var err error // Check picture exists first. if _, err = os.Stat(picture); os.IsNotExist(err) { @@ -117,7 +149,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { } file, _ := os.ReadFile(filepath.Clean(picture)) _, name := filepath.Split(picture) - return f.AddPictureFromBytes(sheet, cell, format, name, ext, file) + return f.AddPictureFromBytes(sheet, cell, name, ext, file, opts) } // AddPictureFromBytes provides the method to add picture in a sheet by given @@ -143,24 +175,21 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // if err != nil { // fmt.Println(err) // } -// if err := f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file); err != nil { +// if err := f.AddPictureFromBytes("Sheet1", "A2", "Excel Logo", ".jpg", file, nil); err != nil { // fmt.Println(err) // } // if err := f.SaveAs("Book1.xlsx"); err != nil { // fmt.Println(err) // } // } -func (f *File) AddPictureFromBytes(sheet, cell, opts, name, extension string, file []byte) error { +func (f *File) AddPictureFromBytes(sheet, cell, name, extension string, file []byte, opts *PictureOptions) error { var drawingHyperlinkRID int var hyperlinkType string ext, ok := supportedImageTypes[extension] if !ok { return ErrImgExt } - options, err := parsePictureOptions(opts) - if err != nil { - return err - } + options := parsePictureOptions(opts) img, _, err := image.DecodeConfig(bytes.NewReader(file)) if err != nil { return err @@ -276,20 +305,20 @@ func (f *File) countDrawings() int { // addDrawingPicture provides a function to add picture by given sheet, // drawingXML, cell, file name, width, height relationship index and format // sets. -func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, hyperlinkRID int, img image.Config, opts *pictureOptions) error { +func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, hyperlinkRID int, img image.Config, opts *PictureOptions) error { col, row, err := CellNameToCoordinates(cell) if err != nil { return err } width, height := img.Width, img.Height - if opts.Autofit { + if opts.AutoFit { width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts) if err != nil { return err } } else { - width = int(float64(width) * opts.XScale) - height = int(float64(height) * opts.YScale) + width = int(float64(width) * *opts.XScale) + height = int(float64(height) * *opts.YScale) } col-- row-- @@ -313,7 +342,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, twoCellAnchor.From = &from twoCellAnchor.To = &to pic := xlsxPic{} - pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = opts.NoChangeAspect + pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = opts.LockAspectRatio pic.NvPicPr.CNvPr.ID = cNvPrID pic.NvPicPr.CNvPr.Descr = file pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID) @@ -342,8 +371,8 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, twoCellAnchor.Pic = &pic twoCellAnchor.ClientData = &xdrClientData{ - FLocksWithSheet: opts.FLocksWithSheet, - FPrintsWithSheet: opts.FPrintsWithSheet, + FLocksWithSheet: *opts.Locked, + FPrintsWithSheet: *opts.PrintObject, } content.Lock() defer content.Unlock() @@ -682,7 +711,7 @@ func (f *File) drawingsWriter() { } // drawingResize calculate the height and width after resizing. -func (f *File) drawingResize(sheet, cell string, width, height float64, opts *pictureOptions) (w, h, c, r int, err error) { +func (f *File) drawingResize(sheet, cell string, width, height float64, opts *PictureOptions) (w, h, c, r int, err error) { var mergeCells []MergeCell mergeCells, err = f.GetMergeCells(sheet) if err != nil { @@ -725,6 +754,6 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *pi height, width = float64(cellHeight), width*asp } width, height = width-float64(opts.OffsetX), height-float64(opts.OffsetY) - w, h = int(width*opts.XScale), int(height*opts.YScale) + w, h = int(width**opts.XScale), int(height**opts.YScale) return } diff --git a/picture_test.go b/picture_test.go index 23923142fb..11243b7655 100644 --- a/picture_test.go +++ b/picture_test.go @@ -25,7 +25,7 @@ func BenchmarkAddPictureFromBytes(b *testing.B) { } b.ResetTimer() for i := 1; i <= b.N; i++ { - if err := f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "excel", ".png", imgFile); err != nil { + if err := f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "excel", ".png", imgFile, nil); err != nil { b.Error(err) } } @@ -37,32 +37,33 @@ func TestAddPicture(t *testing.T) { // Test add picture to worksheet with offset and location hyperlink assert.NoError(t, f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), - `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`)) + &PictureOptions{OffsetX: 140, OffsetY: 120, Hyperlink: "#Sheet2!D8", HyperlinkType: "Location"})) // Test add picture to worksheet with offset, external hyperlink and positioning assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), - `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)) + &PictureOptions{OffsetX: 10, OffsetY: 10, Hyperlink: "https://github.com/xuri/excelize", HyperlinkType: "External", Positioning: "oneCell"})) file, err := os.ReadFile(filepath.Join("test", "images", "excel.png")) assert.NoError(t, err) // Test add picture to worksheet with autofit - assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`)) - assert.NoError(t, f.AddPicture("Sheet1", "B30", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 10, "y_offset": 10, "autofit": true}`)) - f.NewSheet("AddPicture") + assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), &PictureOptions{AutoFit: true})) + assert.NoError(t, f.AddPicture("Sheet1", "B30", filepath.Join("test", "images", "excel.jpg"), &PictureOptions{OffsetX: 10, OffsetY: 10, AutoFit: true})) + _, err = f.NewSheet("AddPicture") + assert.NoError(t, err) assert.NoError(t, f.SetRowHeight("AddPicture", 10, 30)) assert.NoError(t, f.MergeCell("AddPicture", "B3", "D9")) assert.NoError(t, f.MergeCell("AddPicture", "B1", "D1")) - assert.NoError(t, f.AddPicture("AddPicture", "C6", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`)) - assert.NoError(t, f.AddPicture("AddPicture", "A1", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`)) + assert.NoError(t, f.AddPicture("AddPicture", "C6", filepath.Join("test", "images", "excel.jpg"), &PictureOptions{AutoFit: true})) + assert.NoError(t, f.AddPicture("AddPicture", "A1", filepath.Join("test", "images", "excel.jpg"), &PictureOptions{AutoFit: true})) // Test add picture to worksheet from bytes - assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file)) + assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "Excel Logo", ".png", file, nil)) // Test add picture to worksheet from bytes with illegal cell reference - assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", "", "Excel Logo", ".png", file), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", "Excel Logo", ".png", file, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.NoError(t, f.AddPicture("Sheet1", "Q8", filepath.Join("test", "images", "excel.gif"), "")) - assert.NoError(t, f.AddPicture("Sheet1", "Q15", filepath.Join("test", "images", "excel.jpg"), "")) - assert.NoError(t, f.AddPicture("Sheet1", "Q22", filepath.Join("test", "images", "excel.tif"), "")) + assert.NoError(t, f.AddPicture("Sheet1", "Q8", filepath.Join("test", "images", "excel.gif"), nil)) + assert.NoError(t, f.AddPicture("Sheet1", "Q15", filepath.Join("test", "images", "excel.jpg"), nil)) + assert.NoError(t, f.AddPicture("Sheet1", "Q22", filepath.Join("test", "images", "excel.tif"), nil)) // Test write file to given path assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx"))) @@ -72,10 +73,10 @@ func TestAddPicture(t *testing.T) { f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "Q1", "Excel Logo", ".png", file, nil), "XML syntax error on line 1: invalid UTF-8") // Test add picture with invalid sheet name - assert.EqualError(t, f.AddPicture("Sheet:1", "A1", filepath.Join("test", "images", "excel.jpg"), ""), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddPicture("Sheet:1", "A1", filepath.Join("test", "images", "excel.jpg"), nil), ErrSheetNameInvalid.Error()) } func TestAddPictureErrors(t *testing.T) { @@ -83,14 +84,14 @@ func TestAddPictureErrors(t *testing.T) { assert.NoError(t, err) // Test add picture to worksheet with invalid file path - assert.Error(t, f.AddPicture("Sheet1", "G21", filepath.Join("test", "not_exists_dir", "not_exists.icon"), "")) + assert.Error(t, f.AddPicture("Sheet1", "G21", filepath.Join("test", "not_exists_dir", "not_exists.icon"), nil)) // Test add picture to worksheet with unsupported file type - assert.EqualError(t, f.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), ""), ErrImgExt.Error()) - assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)), ErrImgExt.Error()) + assert.EqualError(t, f.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), nil), ErrImgExt.Error()) + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", "Excel Logo", "jpg", make([]byte, 1), nil), ErrImgExt.Error()) // Test add picture to worksheet with invalid file data - assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)), image.ErrFormat.Error()) + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", "Excel Logo", ".jpg", make([]byte, 1), nil), image.ErrFormat.Error()) // Test add picture with custom image decoder and encoder decode := func(r io.Reader) (image.Image, error) { return nil, nil } @@ -100,18 +101,19 @@ func TestAddPictureErrors(t *testing.T) { image.RegisterFormat("emz", "", decode, decodeConfig) image.RegisterFormat("wmz", "", decode, decodeConfig) image.RegisterFormat("svg", "", decode, decodeConfig) - assert.NoError(t, f.AddPicture("Sheet1", "Q1", filepath.Join("test", "images", "excel.emf"), "")) - assert.NoError(t, f.AddPicture("Sheet1", "Q7", filepath.Join("test", "images", "excel.wmf"), "")) - assert.NoError(t, f.AddPicture("Sheet1", "Q13", filepath.Join("test", "images", "excel.emz"), "")) - assert.NoError(t, f.AddPicture("Sheet1", "Q19", filepath.Join("test", "images", "excel.wmz"), "")) - assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", `{"x_scale": 2.1}`)) + assert.NoError(t, f.AddPicture("Sheet1", "Q1", filepath.Join("test", "images", "excel.emf"), nil)) + assert.NoError(t, f.AddPicture("Sheet1", "Q7", filepath.Join("test", "images", "excel.wmf"), nil)) + assert.NoError(t, f.AddPicture("Sheet1", "Q13", filepath.Join("test", "images", "excel.emz"), nil)) + assert.NoError(t, f.AddPicture("Sheet1", "Q19", filepath.Join("test", "images", "excel.wmz"), nil)) + xScale := 2.1 + assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", &PictureOptions{XScale: &xScale})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx"))) assert.NoError(t, f.Close()) } func TestGetPicture(t *testing.T) { f := NewFile() - assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), "")) + assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil)) name, content, err := f.GetPicture("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, 13233, len(content)) @@ -196,19 +198,20 @@ func TestGetPicture(t *testing.T) { func TestAddDrawingPicture(t *testing.T) { // Test addDrawingPicture with illegal cell reference f := NewFile() - assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", "", 0, 0, image.Config{}, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + opts := &PictureOptions{PrintObject: boolPtr(true), Locked: boolPtr(false), XScale: float64Ptr(defaultPictureScale), YScale: float64Ptr(defaultPictureScale)} + assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", "", 0, 0, image.Config{}, opts), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) - assert.EqualError(t, f.addDrawingPicture("sheet1", path, "A1", "", "", 0, 0, image.Config{}, &pictureOptions{}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addDrawingPicture("sheet1", path, "A1", "", "", 0, 0, image.Config{}, opts), "XML syntax error on line 1: invalid UTF-8") } func TestAddPictureFromBytes(t *testing.T) { f := NewFile() imgFile, err := os.ReadFile("logo.png") assert.NoError(t, err, "Unable to load logo for test") - assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile)) - assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "", "logo", ".png", imgFile)) + assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "logo", ".png", imgFile, nil)) + assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "logo", ".png", imgFile, nil)) imageCount := 0 f.Pkg.Range(func(fileName, v interface{}) bool { if strings.Contains(fileName.(string), "media/image") { @@ -217,16 +220,16 @@ func TestAddPictureFromBytes(t *testing.T) { return true }) assert.Equal(t, 1, imageCount, "Duplicate image should only be stored once.") - assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), "", "logo", ".png", imgFile), "sheet SheetN does not exist") + assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), "logo", ".png", imgFile, nil), "sheet SheetN does not exist") // Test add picture from bytes with invalid sheet name - assert.EqualError(t, f.AddPictureFromBytes("Sheet:1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddPictureFromBytes("Sheet:1", fmt.Sprint("A", 1), "logo", ".png", imgFile, nil), ErrSheetNameInvalid.Error()) } func TestDeletePicture(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) assert.NoError(t, f.DeletePicture("Sheet1", "A1")) - assert.NoError(t, f.AddPicture("Sheet1", "P1", filepath.Join("test", "images", "excel.jpg"), "")) + assert.NoError(t, f.AddPicture("Sheet1", "P1", filepath.Join("test", "images", "excel.jpg"), nil)) assert.NoError(t, f.DeletePicture("Sheet1", "P1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture.xlsx"))) // Test delete picture on not exists worksheet @@ -251,7 +254,7 @@ func TestDrawingResize(t *testing.T) { ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} - assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), &PictureOptions{AutoFit: true}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestSetContentTypePartImageExtensions(t *testing.T) { diff --git a/pivotTable.go b/pivotTable.go index 7b4b5535b4..381938edc4 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -27,26 +27,26 @@ import ( // PivotStyleDark1 - PivotStyleDark28 type PivotTableOptions struct { pivotTableSheetName string - DataRange string `json:"data_range"` - PivotTableRange string `json:"pivot_table_range"` - Rows []PivotTableField `json:"rows"` - Columns []PivotTableField `json:"columns"` - Data []PivotTableField `json:"data"` - Filter []PivotTableField `json:"filter"` - RowGrandTotals bool `json:"row_grand_totals"` - ColGrandTotals bool `json:"col_grand_totals"` - ShowDrill bool `json:"show_drill"` - UseAutoFormatting bool `json:"use_auto_formatting"` - PageOverThenDown bool `json:"page_over_then_down"` - MergeItem bool `json:"merge_item"` - CompactData bool `json:"compact_data"` - ShowError bool `json:"show_error"` - ShowRowHeaders bool `json:"show_row_headers"` - ShowColHeaders bool `json:"show_col_headers"` - ShowRowStripes bool `json:"show_row_stripes"` - ShowColStripes bool `json:"show_col_stripes"` - ShowLastColumn bool `json:"show_last_column"` - PivotTableStyleName string `json:"pivot_table_style_name"` + DataRange string + PivotTableRange string + Rows []PivotTableField + Columns []PivotTableField + Data []PivotTableField + Filter []PivotTableField + RowGrandTotals bool + ColGrandTotals bool + ShowDrill bool + UseAutoFormatting bool + PageOverThenDown bool + MergeItem bool + CompactData bool + ShowError bool + ShowRowHeaders bool + ShowColHeaders bool + ShowRowStripes bool + ShowColStripes bool + ShowLastColumn bool + PivotTableStyleName string } // PivotTableField directly maps the field settings of the pivot table. @@ -69,12 +69,12 @@ type PivotTableOptions struct { // Name specifies the name of the data field. Maximum 255 characters // are allowed in data field name, excess characters will be truncated. type PivotTableField struct { - Compact bool `json:"compact"` - Data string `json:"data"` - Name string `json:"name"` - Outline bool `json:"outline"` - Subtotal string `json:"subtotal"` - DefaultSubtotal bool `json:"default_subtotal"` + Compact bool + Data string + Name string + Outline bool + Subtotal string + DefaultSubtotal bool } // AddPivotTable provides the method to add pivot table by given pivot table diff --git a/pivotTable_test.go b/pivotTable_test.go index 206388c59b..fbf60b36b3 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -109,7 +109,8 @@ func TestAddPivotTable(t *testing.T) { ShowLastColumn: true, PivotTableStyleName: "PivotStyleLight19", })) - f.NewSheet("Sheet2") + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet2!$A$1:$AR$15", @@ -232,7 +233,7 @@ func TestAddPivotTable(t *testing.T) { Rows: []PivotTableField{{Data: "Year"}}, }), ErrSheetNameInvalid.Error()) // Test adjust range with invalid range - _, _, err := f.adjustRange("") + _, _, err = f.adjustRange("") assert.EqualError(t, err, ErrParameterRequired.Error()) // Test adjust range with incorrect range _, _, err = f.adjustRange("sheet1!") diff --git a/rows_test.go b/rows_test.go index 70ad48b185..20b7a8937b 100644 --- a/rows_test.go +++ b/rows_test.go @@ -189,7 +189,8 @@ func TestRowHeight(t *testing.T) { // Test set row height with custom default row height with prepare XML assert.NoError(t, f.SetCellValue(sheet1, "A10", "A10")) - f.NewSheet("Sheet2") + _, err = f.NewSheet("Sheet2") + assert.NoError(t, err) assert.NoError(t, f.SetCellValue("Sheet2", "A2", true)) height, err = f.GetRowHeight("Sheet2", 1) assert.NoError(t, err) @@ -258,10 +259,9 @@ func TestSharedStringsReader(t *testing.T) { func TestRowVisibility(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - f.NewSheet("Sheet3") + assert.NoError(t, err) + _, err = f.NewSheet("Sheet3") + assert.NoError(t, err) assert.NoError(t, f.SetRowVisible("Sheet3", 2, false)) assert.NoError(t, f.SetRowVisible("Sheet3", 2, true)) visible, err := f.GetRowVisible("Sheet3", 2) @@ -320,7 +320,7 @@ func TestRemoveRow(t *testing.T) { t.FailNow() } - err = f.AutoFilter(sheet1, "A2", "A2", `{"column":"A","expression":"x != blanks"}`) + err = f.AutoFilter(sheet1, "A2:A2", &AutoFilterOptions{Column: "A", Expression: "x != blanks"}) if !assert.NoError(t, err) { t.FailNow() } @@ -990,9 +990,9 @@ func TestCheckRow(t *testing.T) { func TestSetRowStyle(t *testing.T) { f := NewFile() - style1, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#63BE7B"],"pattern":1}}`) + style1, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"#63BE7B"}, Pattern: 1}}) assert.NoError(t, err) - style2, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) + style2, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"#E0EBF5"}, Pattern: 1}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "B2", "B2", style1)) assert.EqualError(t, f.SetRowStyle("Sheet1", 5, -1, style2), newInvalidRowNumberError(-1).Error()) diff --git a/shape.go b/shape.go index 2022ee6775..3e2db80f2a 100644 --- a/shape.go +++ b/shape.go @@ -12,26 +12,38 @@ package excelize import ( - "encoding/json" "strconv" "strings" ) // parseShapeOptions provides a function to parse the format settings of the // shape with default value. -func parseShapeOptions(opts string) (*shapeOptions, error) { - options := shapeOptions{ - Width: 160, - Height: 160, - Format: pictureOptions{ - FPrintsWithSheet: true, - XScale: 1, - YScale: 1, - }, - Line: lineOptions{Width: 1}, +func parseShapeOptions(opts *Shape) (*Shape, error) { + if opts == nil { + return nil, ErrParameterInvalid + } + if opts.Width == nil { + opts.Width = intPtr(defaultShapeSize) + } + if opts.Height == nil { + opts.Height = intPtr(defaultShapeSize) + } + if opts.Format.PrintObject == nil { + opts.Format.PrintObject = boolPtr(true) + } + if opts.Format.Locked == nil { + opts.Format.Locked = boolPtr(false) + } + if opts.Format.XScale == nil { + opts.Format.XScale = float64Ptr(defaultPictureScale) + } + if opts.Format.YScale == nil { + opts.Format.YScale = float64Ptr(defaultPictureScale) + } + if opts.Line.Width == nil { + opts.Line.Width = float64Ptr(defaultShapeLineWidth) } - err := json.Unmarshal([]byte(opts), &options) - return &options, err + return opts, nil } // AddShape provides the method to add shape in a sheet by given worksheet @@ -39,33 +51,29 @@ func parseShapeOptions(opts string) (*shapeOptions, error) { // print settings) and properties set. For example, add text box (rect shape) // in Sheet1: // -// err := f.AddShape("Sheet1", "G6", `{ -// "type": "rect", -// "color": -// { -// "line": "#4286F4", -// "fill": "#8eb9ff" +// width, height, lineWidth := 180, 90, 1.2 +// err := f.AddShape("Sheet1", "G6", +// &excelize.Shape{ +// Type: "rect", +// Color: excelize.ShapeColor{Line: "#4286f4", Fill: "#8eb9ff"}, +// Paragraph: []excelize.ShapeParagraph{ +// { +// Text: "Rectangle Shape", +// Font: excelize.Font{ +// Bold: true, +// Italic: true, +// Family: "Times New Roman", +// Size: 36, +// Color: "#777777", +// Underline: "sng", +// }, +// }, +// }, +// Width: &width, +// Height: &height, +// Line: excelize.ShapeLine{Width: &lineWidth}, // }, -// "paragraph": [ -// { -// "text": "Rectangle Shape", -// "font": -// { -// "bold": true, -// "italic": true, -// "family": "Times New Roman", -// "size": 36, -// "color": "#777777", -// "underline": "sng" -// } -// }], -// "width": 180, -// "height": 90, -// "line": -// { -// "width": 1.2 -// } -// }`) +// ) // // The following shows the type of shape supported by excelize: // @@ -277,7 +285,7 @@ func parseShapeOptions(opts string) (*shapeOptions, error) { // wavy // wavyHeavy // wavyDbl -func (f *File) AddShape(sheet, cell, opts string) error { +func (f *File) AddShape(sheet, cell string, opts *Shape) error { options, err := parseShapeOptions(opts) if err != nil { return err @@ -313,7 +321,7 @@ func (f *File) AddShape(sheet, cell, opts string) error { // addDrawingShape provides a function to add preset geometry by given sheet, // drawingXMLand format sets. -func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOptions) error { +func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) error { fromCol, fromRow, err := CellNameToCoordinates(cell) if err != nil { return err @@ -321,8 +329,8 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOption colIdx := fromCol - 1 rowIdx := fromRow - 1 - width := int(float64(opts.Width) * opts.Format.XScale) - height := int(float64(opts.Height) * opts.Format.YScale) + width := int(float64(*opts.Width) * *opts.Format.XScale) + height := int(float64(*opts.Height) * *opts.Format.YScale) colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.Format.OffsetX, opts.Format.OffsetY, width, height) @@ -381,9 +389,9 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOption }, }, } - if opts.Line.Width != 1 { + if *opts.Line.Width != 1 { shape.SpPr.Ln = xlsxLineProperties{ - W: f.ptToEMUs(opts.Line.Width), + W: f.ptToEMUs(*opts.Line.Width), } } defaultFont, err := f.GetDefaultFont() @@ -391,7 +399,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOption return err } if len(opts.Paragraph) < 1 { - opts.Paragraph = []shapeParagraphOptions{ + opts.Paragraph = []ShapeParagraph{ { Font: Font{ Bold: false, @@ -443,8 +451,8 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *shapeOption } twoCellAnchor.Sp = &shape twoCellAnchor.ClientData = &xdrClientData{ - FLocksWithSheet: opts.Format.FLocksWithSheet, - FPrintsWithSheet: opts.Format.FPrintsWithSheet, + FLocksWithSheet: *opts.Format.Locked, + FPrintsWithSheet: *opts.Format.PrintObject, } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) f.Drawings.Store(drawingXML, content) diff --git a/shape_test.go b/shape_test.go index 4c47d5870e..bddc8d22de 100644 --- a/shape_test.go +++ b/shape_test.go @@ -12,97 +12,88 @@ func TestAddShape(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - - assert.NoError(t, f.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`)) - assert.NoError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`)) - assert.NoError(t, f.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`)) - assert.EqualError(t, f.AddShape("Sheet3", "H1", `{ - "type": "ellipseRibbon", - "color": - { - "line": "#4286f4", - "fill": "#8eb9ff" + shape := &Shape{ + Type: "rect", + Paragraph: []ShapeParagraph{ + {Text: "Rectangle", Font: Font{Color: "CD5C5C"}}, + {Text: "Shape", Font: Font{Bold: true, Color: "2980B9"}}, }, - "paragraph": [ - { - "font": - { - "bold": true, - "italic": true, - "family": "Times New Roman", - "size": 36, - "color": "#777777", - "underline": "single" - } - }], - "height": 90 - }`), "sheet Sheet3 does not exist") - assert.EqualError(t, f.AddShape("Sheet3", "H1", ""), "unexpected end of JSON input") - assert.EqualError(t, f.AddShape("Sheet1", "A", `{ - "type": "rect", - "paragraph": [ - { - "text": "Rectangle", - "font": - { - "color": "CD5C5C" - } + } + assert.NoError(t, f.AddShape("Sheet1", "A30", shape)) + assert.NoError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []ShapeParagraph{{Text: "Rectangle"}, {}}})) + assert.NoError(t, f.AddShape("Sheet1", "C30", &Shape{Type: "rect"})) + assert.EqualError(t, f.AddShape("Sheet3", "H1", + &Shape{ + Type: "ellipseRibbon", + Color: ShapeColor{Line: "#4286f4", Fill: "#8eb9ff"}, + Paragraph: []ShapeParagraph{ + { + Font: Font{ + Bold: true, + Italic: true, + Family: "Times New Roman", + Size: 36, + Color: "#777777", + Underline: "single", + }, + }, + }, }, - { - "text": "Shape", - "font": - { - "bold": true, - "color": "2980B9" - } - }] - }`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + ), "sheet Sheet3 does not exist") + assert.EqualError(t, f.AddShape("Sheet3", "H1", nil), ErrParameterInvalid.Error()) + assert.EqualError(t, f.AddShape("Sheet1", "A", shape), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) // Test add first shape for given sheet f = NewFile() - assert.NoError(t, f.AddShape("Sheet1", "A1", `{ - "type": "ellipseRibbon", - "color": - { - "line": "#4286f4", - "fill": "#8eb9ff" - }, - "paragraph": [ - { - "font": - { - "bold": true, - "italic": true, - "family": "Times New Roman", - "size": 36, - "color": "#777777", - "underline": "single" - } - }], - "height": 90, - "line": - { - "width": 1.2 - } - }`)) + width, height := 1.2, 90 + assert.NoError(t, f.AddShape("Sheet1", "A1", + &Shape{ + Type: "ellipseRibbon", + Color: ShapeColor{Line: "#4286f4", Fill: "#8eb9ff"}, + Paragraph: []ShapeParagraph{ + { + Font: Font{ + Bold: true, + Italic: true, + Family: "Times New Roman", + Size: 36, + Color: "#777777", + Underline: "single", + }, + }, + }, + Height: &height, + Line: ShapeLine{Width: &width}, + })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) // Test add shape with invalid sheet name - assert.EqualError(t, f.AddShape("Sheet:1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddShape("Sheet:1", "A30", shape), ErrSheetNameInvalid.Error()) // Test add shape with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []ShapeParagraph{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") // Test add shape with unsupported charset content types f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []ShapeParagraph{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") } func TestAddDrawingShape(t *testing.T) { f := NewFile() path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) - assert.EqualError(t, f.addDrawingShape("sheet1", path, "A1", &shapeOptions{}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addDrawingShape("sheet1", path, "A1", + &Shape{ + Width: intPtr(defaultShapeSize), + Height: intPtr(defaultShapeSize), + Format: PictureOptions{ + PrintObject: boolPtr(true), + Locked: boolPtr(false), + XScale: float64Ptr(defaultPictureScale), + YScale: float64Ptr(defaultPictureScale), + }, + }, + ), "XML syntax error on line 1: invalid UTF-8") } diff --git a/sheet.go b/sheet.go index 16c4c16ca9..cbafdd2800 100644 --- a/sheet.go +++ b/sheet.go @@ -13,7 +13,6 @@ package excelize import ( "bytes" - "encoding/json" "encoding/xml" "fmt" "io" @@ -45,7 +44,7 @@ func (f *File) NewSheet(sheet string) (int, error) { if index != -1 { return index, err } - f.DeleteSheet(sheet) + _ = f.DeleteSheet(sheet) f.SheetCount++ wb, _ := f.workbookReader() sheetID := 0 @@ -682,19 +681,24 @@ func (f *File) copySheet(from, to int) error { return err } -// SetSheetVisible provides a function to set worksheet visible by given worksheet -// name. A workbook must contain at least one visible worksheet. If the given -// worksheet has been activated, this setting will be invalidated. Sheet state -// values as defined by https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetstatevalues -// -// visible -// hidden -// veryHidden +// getSheetState returns sheet visible enumeration by given hidden status. +func getSheetState(visible bool, veryHidden []bool) string { + state := "hidden" + if !visible && len(veryHidden) > 0 && veryHidden[0] { + state = "veryHidden" + } + return state +} + +// SetSheetVisible provides a function to set worksheet visible by given +// worksheet name. A workbook must contain at least one visible worksheet. If +// the given worksheet has been activated, this setting will be invalidated. +// The third optional veryHidden parameter only works when visible was false. // // For example, hide Sheet1: // // err := f.SetSheetVisible("Sheet1", false) -func (f *File) SetSheetVisible(sheet string, visible bool) error { +func (f *File) SetSheetVisible(sheet string, visible bool, veryHidden ...bool) error { if err := checkSheetName(sheet); err != nil { return err } @@ -710,9 +714,9 @@ func (f *File) SetSheetVisible(sheet string, visible bool) error { } return err } - count := 0 + count, state := 0, getSheetState(visible, veryHidden) for _, v := range wb.Sheets.Sheet { - if v.State != "hidden" { + if v.State != state { count++ } } @@ -726,45 +730,37 @@ func (f *File) SetSheetVisible(sheet string, visible bool) error { tabSelected = ws.SheetViews.SheetView[0].TabSelected } if strings.EqualFold(v.Name, sheet) && count > 1 && !tabSelected { - wb.Sheets.Sheet[k].State = "hidden" + wb.Sheets.Sheet[k].State = state } } return err } -// parsePanesOptions provides a function to parse the panes settings. -func parsePanesOptions(opts string) (*panesOptions, error) { - format := panesOptions{} - err := json.Unmarshal([]byte(opts), &format) - return &format, err -} - // setPanes set create freeze panes and split panes by given options. -func (ws *xlsxWorksheet) setPanes(panes string) error { - opts, err := parsePanesOptions(panes) - if err != nil { - return err +func (ws *xlsxWorksheet) setPanes(panes *Panes) error { + if panes == nil { + return ErrParameterInvalid } p := &xlsxPane{ - ActivePane: opts.ActivePane, - TopLeftCell: opts.TopLeftCell, - XSplit: float64(opts.XSplit), - YSplit: float64(opts.YSplit), + ActivePane: panes.ActivePane, + TopLeftCell: panes.TopLeftCell, + XSplit: float64(panes.XSplit), + YSplit: float64(panes.YSplit), } - if opts.Freeze { + if panes.Freeze { p.State = "frozen" } if ws.SheetViews == nil { ws.SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{{}}} } ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = p - if !(opts.Freeze) && !(opts.Split) { + if !(panes.Freeze) && !(panes.Split) { if len(ws.SheetViews.SheetView) > 0 { ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = nil } } var s []*xlsxSelection - for _, p := range opts.Panes { + for _, p := range panes.Panes { s = append(s, &xlsxSelection{ ActiveCell: p.ActiveCell, Pane: p.Pane, @@ -772,94 +768,128 @@ func (ws *xlsxWorksheet) setPanes(panes string) error { }) } ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Selection = s - return err + return nil } // SetPanes provides a function to create and remove freeze panes and split panes // by given worksheet name and panes options. // -// activePane defines the pane that is active. The possible values for this +// ActivePane defines the pane that is active. The possible values for this // attribute are defined in the following table: // -// Enumeration Value | Description -// --------------------------------+------------------------------------------------------------- -// bottomLeft (Bottom Left Pane) | Bottom left pane, when both vertical and horizontal -// | splits are applied. -// | -// | This value is also used when only a horizontal split has -// | been applied, dividing the pane into upper and lower -// | regions. In that case, this value specifies the bottom -// | pane. -// | -// bottomRight (Bottom Right Pane) | Bottom right pane, when both vertical and horizontal -// | splits are applied. -// | -// topLeft (Top Left Pane) | Top left pane, when both vertical and horizontal splits -// | are applied. -// | -// | This value is also used when only a horizontal split has -// | been applied, dividing the pane into upper and lower -// | regions. In that case, this value specifies the top pane. -// | -// | This value is also used when only a vertical split has -// | been applied, dividing the pane into right and left -// | regions. In that case, this value specifies the left pane -// | -// topRight (Top Right Pane) | Top right pane, when both vertical and horizontal -// | splits are applied. -// | -// | This value is also used when only a vertical split has -// | been applied, dividing the pane into right and left -// | regions. In that case, this value specifies the right -// | pane. +// Enumeration Value | Description +// ---------------------------------+------------------------------------------------------------- +// bottomLeft (Bottom Left Pane) | Bottom left pane, when both vertical and horizontal +// | splits are applied. +// | +// | This value is also used when only a horizontal split has +// | been applied, dividing the pane into upper and lower +// | regions. In that case, this value specifies the bottom +// | pane. +// | +// bottomRight (Bottom Right Pane) | Bottom right pane, when both vertical and horizontal +// | splits are applied. +// | +// topLeft (Top Left Pane) | Top left pane, when both vertical and horizontal splits +// | are applied. +// | +// | This value is also used when only a horizontal split has +// | been applied, dividing the pane into upper and lower +// | regions. In that case, this value specifies the top pane. +// | +// | This value is also used when only a vertical split has +// | been applied, dividing the pane into right and left +// | regions. In that case, this value specifies the left pane +// | +// topRight (Top Right Pane) | Top right pane, when both vertical and horizontal +// | splits are applied. +// | +// | This value is also used when only a vertical split has +// | been applied, dividing the pane into right and left +// | regions. In that case, this value specifies the right +// | pane. // // Pane state type is restricted to the values supported currently listed in the following table: // -// Enumeration Value | Description -// --------------------------------+------------------------------------------------------------- -// frozen (Frozen) | Panes are frozen, but were not split being frozen. In -// | this state, when the panes are unfrozen again, a single -// | pane results, with no split. -// | -// | In this state, the split bars are not adjustable. -// | -// split (Split) | Panes are split, but not frozen. In this state, the split -// | bars are adjustable by the user. +// Enumeration Value | Description +// ---------------------------------+------------------------------------------------------------- +// frozen (Frozen) | Panes are frozen, but were not split being frozen. In +// | this state, when the panes are unfrozen again, a single +// | pane results, with no split. +// | +// | In this state, the split bars are not adjustable. +// | +// split (Split) | Panes are split, but not frozen. In this state, the split +// | bars are adjustable by the user. // -// x_split (Horizontal Split Position): Horizontal position of the split, in +// XSplit (Horizontal Split Position): Horizontal position of the split, in // 1/20th of a point; 0 (zero) if none. If the pane is frozen, this value // indicates the number of columns visible in the top pane. // -// y_split (Vertical Split Position): Vertical position of the split, in 1/20th +// YSplit (Vertical Split Position): Vertical position of the split, in 1/20th // of a point; 0 (zero) if none. If the pane is frozen, this value indicates the // number of rows visible in the left pane. The possible values for this // attribute are defined by the W3C XML Schema double datatype. // -// top_left_cell: Location of the top left visible cell in the bottom right pane +// TopLeftCell: Location of the top left visible cell in the bottom right pane // (when in Left-To-Right mode). // -// sqref (Sequence of References): Range of the selection. Can be non-contiguous +// SQRef (Sequence of References): Range of the selection. Can be non-contiguous // set of ranges. // // An example of how to freeze column A in the Sheet1 and set the active cell on // Sheet1!K16: // -// f.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`) +// err := f.SetPanes("Sheet1", &excelize.Panes{ +// Freeze: true, +// Split: false, +// XSplit: 1, +// YSplit: 0, +// TopLeftCell: "B1", +// ActivePane: "topRight", +// Panes: []excelize.PaneOptions{ +// {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, +// }, +// }) // // An example of how to freeze rows 1 to 9 in the Sheet1 and set the active cell // ranges on Sheet1!A11:XFD11: // -// f.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`) +// err := f.SetPanes("Sheet1", &excelize.Panes{ +// Freeze: true, +// Split: false, +// XSplit: 0, +// YSplit: 9, +// TopLeftCell: "A34", +// ActivePane: "bottomLeft", +// Panes: []excelize.PaneOptions{ +// {SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"}, +// }, +// }) // // An example of how to create split panes in the Sheet1 and set the active cell // on Sheet1!J60: // -// f.SetPanes("Sheet1", `{"freeze":false,"split":true,"x_split":3270,"y_split":1800,"top_left_cell":"N57","active_pane":"bottomLeft","panes":[{"sqref":"I36","active_cell":"I36"},{"sqref":"G33","active_cell":"G33","pane":"topRight"},{"sqref":"J60","active_cell":"J60","pane":"bottomLeft"},{"sqref":"O60","active_cell":"O60","pane":"bottomRight"}]}`) +// err := f.SetPanes("Sheet1", &excelize.Panes{ +// Freeze: false, +// Split: true, +// XSplit: 3270, +// YSplit: 1800, +// TopLeftCell: "N57", +// ActivePane: "bottomLeft", +// Panes: []excelize.PaneOptions{ +// {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"}, +// {SQRef: "I36", ActiveCell: "I36"}, +// {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"}, +// {SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"}, +// {SQRef: "O60", ActiveCell: "O60", Pane: "bottomRight"}, +// }, +// }) // // An example of how to unfreeze and remove all panes on Sheet1: // -// f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) -func (f *File) SetPanes(sheet, panes string) error { +// err := f.SetPanes("Sheet1", &excelize.Panes{Freeze: false, Split: false}) +func (f *File) SetPanes(sheet string, panes *Panes) error { ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -870,7 +900,7 @@ func (f *File) SetPanes(sheet, panes string) error { // GetSheetVisible provides a function to get worksheet visible by given worksheet // name. For example, get visible state of Sheet1: // -// f.GetSheetVisible("Sheet1") +// visible, err := f.GetSheetVisible("Sheet1") func (f *File) GetSheetVisible(sheet string) (bool, error) { var visible bool if err := checkSheetName(sheet); err != nil { @@ -1509,6 +1539,9 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) { // Scope: "Sheet2", // }) func (f *File) SetDefinedName(definedName *DefinedName) error { + if definedName.Name == "" || definedName.RefersTo == "" { + return ErrParameterInvalid + } wb, err := f.workbookReader() if err != nil { return err diff --git a/sheet_test.go b/sheet_test.go index ed85c264a2..09b6155422 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -15,14 +15,15 @@ import ( func TestNewSheet(t *testing.T) { f := NewFile() - f.NewSheet("Sheet2") + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) sheetID, err := f.NewSheet("sheet2") assert.NoError(t, err) f.SetActiveSheet(sheetID) // Test delete original sheet idx, err := f.GetSheetIndex("Sheet1") assert.NoError(t, err) - f.DeleteSheet(f.GetSheetName(idx)) + assert.NoError(t, f.DeleteSheet(f.GetSheetName(idx))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewSheet.xlsx"))) // Test create new worksheet with already exists name sheetID, err = f.NewSheet("Sheet2") @@ -38,24 +39,79 @@ func TestNewSheet(t *testing.T) { func TestSetPanes(t *testing.T) { f := NewFile() - assert.NoError(t, f.SetPanes("Sheet1", `{"freeze":false,"split":false}`)) - f.NewSheet("Panes 2") - assert.NoError(t, f.SetPanes("Panes 2", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`)) - f.NewSheet("Panes 3") - assert.NoError(t, f.SetPanes("Panes 3", `{"freeze":false,"split":true,"x_split":3270,"y_split":1800,"top_left_cell":"N57","active_pane":"bottomLeft","panes":[{"sqref":"I36","active_cell":"I36"},{"sqref":"G33","active_cell":"G33","pane":"topRight"},{"sqref":"J60","active_cell":"J60","pane":"bottomLeft"},{"sqref":"O60","active_cell":"O60","pane":"bottomRight"}]}`)) - f.NewSheet("Panes 4") - assert.NoError(t, f.SetPanes("Panes 4", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`)) - assert.EqualError(t, f.SetPanes("Panes 4", ""), "unexpected end of JSON input") - assert.EqualError(t, f.SetPanes("SheetN", ""), "sheet SheetN does not exist") + + assert.NoError(t, f.SetPanes("Sheet1", &Panes{Freeze: false, Split: false})) + _, err := f.NewSheet("Panes 2") + assert.NoError(t, err) + assert.NoError(t, f.SetPanes("Panes 2", + &Panes{ + Freeze: true, + Split: false, + XSplit: 1, + YSplit: 0, + TopLeftCell: "B1", + ActivePane: "topRight", + Panes: []PaneOptions{ + {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, + }, + }, + )) + _, err = f.NewSheet("Panes 3") + assert.NoError(t, err) + assert.NoError(t, f.SetPanes("Panes 3", + &Panes{ + Freeze: false, + Split: true, + XSplit: 3270, + YSplit: 1800, + TopLeftCell: "N57", + ActivePane: "bottomLeft", + Panes: []PaneOptions{ + {SQRef: "I36", ActiveCell: "I36"}, + {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"}, + {SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"}, + {SQRef: "O60", ActiveCell: "O60", Pane: "bottomRight"}, + }, + }, + )) + _, err = f.NewSheet("Panes 4") + assert.NoError(t, err) + assert.NoError(t, f.SetPanes("Panes 4", + &Panes{ + Freeze: true, + Split: false, + XSplit: 0, + YSplit: 9, + TopLeftCell: "A34", + ActivePane: "bottomLeft", + Panes: []PaneOptions{ + {SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"}, + }, + }, + )) + assert.EqualError(t, f.SetPanes("Panes 4", nil), ErrParameterInvalid.Error()) + assert.EqualError(t, f.SetPanes("SheetN", nil), "sheet SheetN does not exist") // Test set panes with invalid sheet name - assert.EqualError(t, f.SetPanes("Sheet:1", `{"freeze":false,"split":false}`), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.SetPanes("Sheet:1", &Panes{Freeze: false, Split: false}), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) // Test add pane on empty sheet views worksheet f = NewFile() f.checked = nil f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(``)) - assert.NoError(t, f.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`)) + assert.NoError(t, f.SetPanes("Sheet1", + &Panes{ + Freeze: true, + Split: false, + XSplit: 1, + YSplit: 0, + TopLeftCell: "B1", + ActivePane: "topRight", + Panes: []PaneOptions{ + {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, + }, + }, + )) } func TestSearchSheet(t *testing.T) { @@ -108,7 +164,7 @@ func TestSearchSheet(t *testing.T) { assert.EqualError(t, err, "invalid cell reference [1, 0]") assert.Equal(t, []string(nil), result) - // Test search sheet with unsupported charset shared strings table. + // Test search sheet with unsupported charset shared strings table f.SharedStrings = nil f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) _, err = f.SearchSheet("Sheet1", "A") @@ -204,6 +260,14 @@ func TestDefinedName(t *testing.T) { assert.EqualError(t, f.DeleteDefinedName(&DefinedName{ Name: "No Exist Defined Name", }), ErrDefinedNameScope.Error()) + // Test set defined name without name + assert.EqualError(t, f.SetDefinedName(&DefinedName{ + RefersTo: "Sheet1!$A$2:$D$5", + }), ErrParameterInvalid.Error()) + // Test set defined name without reference + assert.EqualError(t, f.SetDefinedName(&DefinedName{ + Name: "Amount", + }), ErrParameterInvalid.Error()) assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[1].RefersTo) assert.NoError(t, f.DeleteDefinedName(&DefinedName{ Name: "Amount", @@ -228,7 +292,8 @@ func TestGroupSheets(t *testing.T) { f := NewFile() sheets := []string{"Sheet2", "Sheet3"} for _, sheet := range sheets { - f.NewSheet(sheet) + _, err := f.NewSheet(sheet) + assert.NoError(t, err) } assert.EqualError(t, f.GroupSheets([]string{"Sheet1", "SheetN"}), "sheet SheetN does not exist") assert.EqualError(t, f.GroupSheets([]string{"Sheet2", "Sheet3"}), "group worksheet must contain an active worksheet") @@ -242,7 +307,8 @@ func TestUngroupSheets(t *testing.T) { f := NewFile() sheets := []string{"Sheet2", "Sheet3", "Sheet4", "Sheet5"} for _, sheet := range sheets { - f.NewSheet(sheet) + _, err := f.NewSheet(sheet) + assert.NoError(t, err) } assert.NoError(t, f.UngroupSheets()) } @@ -276,7 +342,8 @@ func TestRemovePageBreak(t *testing.T) { assert.NoError(t, f.RemovePageBreak("Sheet1", "B3")) assert.NoError(t, f.RemovePageBreak("Sheet1", "A3")) - f.NewSheet("Sheet2") + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) assert.NoError(t, f.InsertPageBreak("Sheet2", "B2")) assert.NoError(t, f.InsertPageBreak("Sheet2", "C2")) assert.NoError(t, f.RemovePageBreak("Sheet2", "B2")) @@ -381,20 +448,23 @@ func TestDeleteSheet(t *testing.T) { idx, err := f.NewSheet("Sheet2") assert.NoError(t, err) f.SetActiveSheet(idx) - f.NewSheet("Sheet3") - f.DeleteSheet("Sheet1") + _, err = f.NewSheet("Sheet3") + assert.NoError(t, err) + assert.NoError(t, f.DeleteSheet("Sheet1")) assert.Equal(t, "Sheet2", f.GetSheetName(f.GetActiveSheetIndex())) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet.xlsx"))) // Test with auto filter defined names f = NewFile() - f.NewSheet("Sheet2") - f.NewSheet("Sheet3") + _, err = f.NewSheet("Sheet2") + assert.NoError(t, err) + _, err = f.NewSheet("Sheet3") + assert.NoError(t, err) assert.NoError(t, f.SetCellValue("Sheet1", "A1", "A")) assert.NoError(t, f.SetCellValue("Sheet2", "A1", "A")) assert.NoError(t, f.SetCellValue("Sheet3", "A1", "A")) - assert.NoError(t, f.AutoFilter("Sheet1", "A1", "A1", "")) - assert.NoError(t, f.AutoFilter("Sheet2", "A1", "A1", "")) - assert.NoError(t, f.AutoFilter("Sheet3", "A1", "A1", "")) + assert.NoError(t, f.AutoFilter("Sheet1", "A1:A1", nil)) + assert.NoError(t, f.AutoFilter("Sheet2", "A1:A1", nil)) + assert.NoError(t, f.AutoFilter("Sheet3", "A1:A1", nil)) assert.NoError(t, f.DeleteSheet("Sheet2")) assert.NoError(t, f.DeleteSheet("Sheet1")) // Test delete sheet with invalid sheet name @@ -408,9 +478,10 @@ func TestDeleteAndAdjustDefinedNames(t *testing.T) { } func TestGetSheetID(t *testing.T) { - file := NewFile() - file.NewSheet("Sheet1") - id := file.getSheetID("sheet1") + f := NewFile() + _, err := f.NewSheet("Sheet1") + assert.NoError(t, err) + id := f.getSheetID("sheet1") assert.NotEqual(t, -1, id) } @@ -444,7 +515,7 @@ func TestGetSheetIndex(t *testing.T) { func TestSetContentTypes(t *testing.T) { f := NewFile() - // Test set content type with unsupported charset content types. + // Test set content type with unsupported charset content types f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.setContentTypes("/xl/worksheets/sheet1.xml", ContentTypeSpreadSheetMLWorksheet), "XML syntax error on line 1: invalid UTF-8") @@ -452,7 +523,7 @@ func TestSetContentTypes(t *testing.T) { func TestDeleteSheetFromContentTypes(t *testing.T) { f := NewFile() - // Test delete sheet from content types with unsupported charset content types. + // Test delete sheet from content types with unsupported charset content types f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.deleteSheetFromContentTypes("/xl/worksheets/sheet1.xml"), "XML syntax error on line 1: invalid UTF-8") @@ -468,9 +539,8 @@ func BenchmarkNewSheet(b *testing.B) { func newSheetWithSet() { file := NewFile() - file.NewSheet("sheet1") for i := 0; i < 1000; i++ { - _ = file.SetCellInt("sheet1", "A"+strconv.Itoa(i+1), i) + _ = file.SetCellInt("Sheet1", "A"+strconv.Itoa(i+1), i) } file = nil } @@ -485,9 +555,8 @@ func BenchmarkFile_SaveAs(b *testing.B) { func newSheetWithSave() { file := NewFile() - file.NewSheet("sheet1") for i := 0; i < 1000; i++ { - _ = file.SetCellInt("sheet1", "A"+strconv.Itoa(i+1), i) + _ = file.SetCellInt("Sheet1", "A"+strconv.Itoa(i+1), i) } _ = file.Save() } @@ -520,12 +589,13 @@ func TestAttrValToFloat(t *testing.T) { func TestSetSheetBackgroundFromBytes(t *testing.T) { f := NewFile() - f.SetSheetName("Sheet1", ".svg") + assert.NoError(t, f.SetSheetName("Sheet1", ".svg")) for i, imageTypes := range []string{".svg", ".emf", ".emz", ".gif", ".jpg", ".png", ".tif", ".wmf", ".wmz"} { file := fmt.Sprintf("excelize%s", imageTypes) if i > 0 { file = filepath.Join("test", "images", fmt.Sprintf("excel%s", imageTypes)) - f.NewSheet(imageTypes) + _, err := f.NewSheet(imageTypes) + assert.NoError(t, err) } img, err := os.Open(file) assert.NoError(t, err) diff --git a/sheetview_test.go b/sheetview_test.go index 8d022a2e0f..b7347775d7 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -28,10 +28,10 @@ func TestSetView(t *testing.T) { opts, err := f.GetSheetView("Sheet1", 0) assert.NoError(t, err) assert.Equal(t, expected, opts) - // Test set sheet view options with invalid view index. + // Test set sheet view options with invalid view index assert.EqualError(t, f.SetSheetView("Sheet1", 1, nil), "view index 1 out of range") assert.EqualError(t, f.SetSheetView("Sheet1", -2, nil), "view index -2 out of range") - // Test set sheet view options on not exists worksheet. + // Test set sheet view options on not exists worksheet assert.EqualError(t, f.SetSheetView("SheetN", 0, nil), "sheet SheetN does not exist") } @@ -39,12 +39,12 @@ func TestGetView(t *testing.T) { f := NewFile() _, err := f.getSheetView("SheetN", 0) assert.EqualError(t, err, "sheet SheetN does not exist") - // Test get sheet view options with invalid view index. + // Test get sheet view options with invalid view index _, err = f.GetSheetView("Sheet1", 1) assert.EqualError(t, err, "view index 1 out of range") _, err = f.GetSheetView("Sheet1", -2) assert.EqualError(t, err, "view index -2 out of range") - // Test get sheet view options on not exists worksheet. + // Test get sheet view options on not exists worksheet _, err = f.GetSheetView("SheetN", 0) assert.EqualError(t, err, "sheet SheetN does not exist") } diff --git a/sparkline_test.go b/sparkline_test.go index c2c1c41330..e6bca25808 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -9,10 +9,11 @@ import ( ) func TestAddSparkline(t *testing.T) { - f := prepareSparklineDataset() + f, err := prepareSparklineDataset() + assert.NoError(t, err) // Set the columns widths to make the output clearer - style, err := f.NewStyle(`{"font":{"bold":true}}`) + style, err := f.NewStyle(&Style{Font: &Font{Bold: true}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "B1", style)) viewOpts, err := f.GetSheetView("Sheet1", 0) @@ -291,7 +292,7 @@ func TestAppendSparkline(t *testing.T) { assert.EqualError(t, f.appendSparkline(ws, &xlsxX14SparklineGroup{}, &xlsxX14SparklineGroups{}), "XML syntax error on line 1: invalid UTF-8") } -func prepareSparklineDataset() *File { +func prepareSparklineDataset() (*File, error) { f := NewFile() sheet2 := [][]int{ {-2, 2, 3, -1, 0}, @@ -307,8 +308,12 @@ func prepareSparklineDataset() *File { {3, -1, 0, -2, 3, 2, 1, 0, 2, 1}, {0, -2, 3, 2, 1, 0, 1, 2, 3, 1}, } - f.NewSheet("Sheet2") - f.NewSheet("Sheet3") + if _, err := f.NewSheet("Sheet2"); err != nil { + return f, err + } + if _, err := f.NewSheet("Sheet3"); err != nil { + return f, err + } for row, data := range sheet2 { if err := f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data); err != nil { fmt.Println(err) @@ -319,5 +324,5 @@ func prepareSparklineDataset() *File { fmt.Println(err) } } - return f + return f, nil } diff --git a/stream.go b/stream.go index 0209e22701..7a17484aff 100644 --- a/stream.go +++ b/stream.go @@ -134,18 +134,19 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { // AddTable creates an Excel table for the StreamWriter using the given // cell range and format set. For example, create a table of A1:D5: // -// err := sw.AddTable("A1", "D5", "") +// err := sw.AddTable("A1:D5", nil) // // Create a table of F2:H6 with format set: // -// err := sw.AddTable("F2", "H6", `{ -// "table_name": "table", -// "table_style": "TableStyleMedium2", -// "show_first_column": true, -// "show_last_column": true, -// "show_row_stripes": false, -// "show_column_stripes": true -// }`) +// disable := false +// err := sw.AddTable("F2:H6", &excelize.TableOptions{ +// Name: "table", +// StyleName: "TableStyleMedium2", +// ShowFirstColumn: true, +// ShowLastColumn: true, +// ShowRowStripes: &disable, +// ShowColumnStripes: true, +// }) // // Note that the table must be at least two lines including the header. The // header cells must contain strings and must be unique. @@ -154,13 +155,9 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { // called after the rows are written but before Flush. // // See File.AddTable for details on the table format. -func (sw *StreamWriter) AddTable(hCell, vCell, opts string) error { - options, err := parseTableOptions(opts) - if err != nil { - return err - } - - coordinates, err := cellRefsToCoordinates(hCell, vCell) +func (sw *StreamWriter) AddTable(reference string, opts *TableOptions) error { + options := parseTableOptions(opts) + coordinates, err := rangeRefToCoordinates(reference) if err != nil { return err } @@ -192,7 +189,7 @@ func (sw *StreamWriter) AddTable(hCell, vCell, opts string) error { tableID := sw.file.countTables() + 1 - name := options.TableName + name := options.Name if name == "" { name = "Table" + strconv.Itoa(tableID) } @@ -211,10 +208,10 @@ func (sw *StreamWriter) AddTable(hCell, vCell, opts string) error { TableColumn: tableColumn, }, TableStyleInfo: &xlsxTableStyleInfo{ - Name: options.TableStyle, + Name: options.StyleName, ShowFirstColumn: options.ShowFirstColumn, ShowLastColumn: options.ShowLastColumn, - ShowRowStripes: options.ShowRowStripes, + ShowRowStripes: *options.ShowRowStripes, ShowColumnStripes: options.ShowColumnStripes, }, } @@ -462,7 +459,7 @@ func (sw *StreamWriter) InsertPageBreak(cell string) error { // SetPanes provides a function to create and remove freeze panes and split // panes by giving panes options for the StreamWriter. Note that you must call // the 'SetPanes' function before the 'SetRow' function. -func (sw *StreamWriter) SetPanes(panes string) error { +func (sw *StreamWriter) SetPanes(panes *Panes) error { if sw.sheetWritten { return ErrStreamSetPanes } diff --git a/stream_test.go b/stream_test.go index 1a63e35fe3..195bdf099f 100644 --- a/stream_test.go +++ b/stream_test.go @@ -41,12 +41,12 @@ func TestStreamWriter(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) - // Test max characters in a cell. + // Test max characters in a cell row := make([]interface{}, 1) row[0] = strings.Repeat("c", TotalCellChars+2) assert.NoError(t, streamWriter.SetRow("A1", row)) - // Test leading and ending space(s) character characters in a cell. + // Test leading and ending space(s) character characters in a cell row = make([]interface{}, 1) row[0] = " characters" assert.NoError(t, streamWriter.SetRow("A2", row)) @@ -55,7 +55,7 @@ func TestStreamWriter(t *testing.T) { row[0] = []byte("Word") assert.NoError(t, streamWriter.SetRow("A3", row)) - // Test set cell with style and rich text. + // Test set cell with style and rich text styleID, err := file.NewStyle(&Style{Font: &Font{Color: "#777777"}}) assert.NoError(t, err) assert.NoError(t, streamWriter.SetRow("A4", []interface{}{ @@ -85,14 +85,14 @@ func TestStreamWriter(t *testing.T) { } assert.NoError(t, streamWriter.Flush()) - // Save spreadsheet by the given path. + // Save spreadsheet by the given path assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) - // Test set cell column overflow. + // Test set cell column overflow assert.ErrorIs(t, streamWriter.SetRow("XFD51201", []interface{}{"A", "B", "C"}), ErrColumnNumber) assert.NoError(t, file.Close()) - // Test close temporary file error. + // Test close temporary file error file = NewFile() streamWriter, err = file.NewStreamWriter("Sheet1") assert.NoError(t, err) @@ -114,7 +114,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, streamWriter.rawData.tmp.Close()) assert.NoError(t, os.Remove(streamWriter.rawData.tmp.Name())) - // Test create stream writer with unsupported charset. + // Test create stream writer with unsupported charset file = NewFile() file.Sheet.Delete("xl/worksheets/sheet1.xml") file.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) @@ -122,7 +122,7 @@ func TestStreamWriter(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, file.Close()) - // Test read cell. + // Test read cell file = NewFile() streamWriter, err = file.NewStreamWriter("Sheet1") assert.NoError(t, err) @@ -132,7 +132,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "Data", cellValue) - // Test stream reader for a worksheet with huge amounts of data. + // Test stream reader for a worksheet with huge amounts of data file, err = OpenFile(filepath.Join("test", "TestStreamWriter.xlsx")) assert.NoError(t, err) rows, err := file.Rows("Sheet1") @@ -166,14 +166,24 @@ func TestStreamSetColWidth(t *testing.T) { } func TestStreamSetPanes(t *testing.T) { - file, paneOpts := NewFile(), `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}` + file, paneOpts := NewFile(), &Panes{ + Freeze: true, + Split: false, + XSplit: 1, + YSplit: 0, + TopLeftCell: "B1", + ActivePane: "topRight", + Panes: []PaneOptions{ + {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, + }, + } defer func() { assert.NoError(t, file.Close()) }() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.SetPanes(paneOpts)) - assert.EqualError(t, streamWriter.SetPanes(""), "unexpected end of JSON input") + assert.EqualError(t, streamWriter.SetPanes(nil), ErrParameterInvalid.Error()) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) assert.ErrorIs(t, streamWriter.SetPanes(paneOpts), ErrStreamSetPanes) } @@ -185,19 +195,20 @@ func TestStreamTable(t *testing.T) { }() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) - - // Write some rows. We want enough rows to force a temp file (>16MB). + // Test add table without table header + assert.EqualError(t, streamWriter.AddTable("A1:C2", nil), "XML syntax error on line 2: unexpected EOF") + // Write some rows. We want enough rows to force a temp file (>16MB) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) row := []interface{}{1, 2, 3} for r := 2; r < 10000; r++ { assert.NoError(t, streamWriter.SetRow(fmt.Sprintf("A%d", r), row)) } - // Write a table. - assert.NoError(t, streamWriter.AddTable("A1", "C2", "")) + // Write a table + assert.NoError(t, streamWriter.AddTable("A1:C2", nil)) assert.NoError(t, streamWriter.Flush()) - // Verify the table has names. + // Verify the table has names var table xlsxTable val, ok := file.Pkg.Load("xl/tables/table1.xml") assert.True(t, ok) @@ -206,17 +217,15 @@ func TestStreamTable(t *testing.T) { assert.Equal(t, "B", table.TableColumns.TableColumn[1].Name) assert.Equal(t, "C", table.TableColumns.TableColumn[2].Name) - assert.NoError(t, streamWriter.AddTable("A1", "C1", "")) + assert.NoError(t, streamWriter.AddTable("A1:C1", nil)) - // Test add table with illegal options. - assert.EqualError(t, streamWriter.AddTable("B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") - // Test add table with illegal cell reference. - assert.EqualError(t, streamWriter.AddTable("A", "B1", `{}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, streamWriter.AddTable("A1", "B", `{}`), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) - // Test add table with unsupported charset content types. + // Test add table with illegal cell reference + assert.EqualError(t, streamWriter.AddTable("A:B1", nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, streamWriter.AddTable("A1:B", nil), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + // Test add table with unsupported charset content types file.ContentTypes = nil file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, streamWriter.AddTable("A1", "C2", ""), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, streamWriter.AddTable("A1:C2", nil), "XML syntax error on line 1: invalid UTF-8") } func TestStreamMergeCells(t *testing.T) { @@ -227,10 +236,10 @@ func TestStreamMergeCells(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.MergeCell("A1", "D1")) - // Test merge cells with illegal cell reference. + // Test merge cells with illegal cell reference assert.EqualError(t, streamWriter.MergeCell("A", "D1"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, streamWriter.Flush()) - // Save spreadsheet by the given path. + // Save spreadsheet by the given path assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamMergeCells.xlsx"))) } @@ -243,7 +252,7 @@ func TestStreamInsertPageBreak(t *testing.T) { assert.NoError(t, err) assert.NoError(t, streamWriter.InsertPageBreak("A1")) assert.NoError(t, streamWriter.Flush()) - // Save spreadsheet by the given path. + // Save spreadsheet by the given path assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamInsertPageBreak.xlsx"))) } @@ -270,7 +279,7 @@ func TestStreamMarshalAttrs(t *testing.T) { } func TestStreamSetRow(t *testing.T) { - // Test error exceptions. + // Test error exceptions file := NewFile() defer func() { assert.NoError(t, file.Close()) @@ -278,10 +287,10 @@ func TestStreamSetRow(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - // Test set row with non-ascending row number. + // Test set row with non-ascending row number assert.NoError(t, streamWriter.SetRow("A1", []interface{}{})) assert.EqualError(t, streamWriter.SetRow("A1", []interface{}{}), newStreamSetRowError(1).Error()) - // Test set row with unsupported charset workbook. + // Test set row with unsupported charset workbook file.WorkBook = nil file.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, streamWriter.SetRow("A2", []interface{}{time.Now()}), "XML syntax error on line 1: invalid UTF-8") @@ -367,13 +376,13 @@ func TestStreamWriterOutlineLevel(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) - // Test set outlineLevel in row. + // Test set outlineLevel in row assert.NoError(t, streamWriter.SetRow("A1", nil, RowOpts{OutlineLevel: 1})) assert.NoError(t, streamWriter.SetRow("A2", nil, RowOpts{OutlineLevel: 7})) assert.ErrorIs(t, ErrOutlineLevel, streamWriter.SetRow("A3", nil, RowOpts{OutlineLevel: 8})) assert.NoError(t, streamWriter.Flush()) - // Save spreadsheet by the given path. + // Save spreadsheet by the given path assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriterSetRowOutlineLevel.xlsx"))) file, err = OpenFile(filepath.Join("test", "TestStreamWriterSetRowOutlineLevel.xlsx")) diff --git a/styles.go b/styles.go index 0f0b560429..6eb86e104b 100644 --- a/styles.go +++ b/styles.go @@ -13,7 +13,6 @@ package excelize import ( "bytes" - "encoding/json" "encoding/xml" "fmt" "io" @@ -1076,35 +1075,25 @@ func (f *File) sharedStringsWriter() { // parseFormatStyleSet provides a function to parse the format settings of the // cells and conditional formats. -func parseFormatStyleSet(style interface{}) (*Style, error) { - fs := Style{} +func parseFormatStyleSet(style *Style) (*Style, error) { var err error - switch v := style.(type) { - case string: - err = json.Unmarshal([]byte(v), &fs) - case *Style: - fs = *v - default: - err = ErrParameterInvalid - } - if fs.Font != nil { - if len(fs.Font.Family) > MaxFontFamilyLength { - return &fs, ErrFontLength + if style.Font != nil { + if len(style.Font.Family) > MaxFontFamilyLength { + return style, ErrFontLength } - if fs.Font.Size > MaxFontSize { - return &fs, ErrFontSize + if style.Font.Size > MaxFontSize { + return style, ErrFontSize } } - if fs.CustomNumFmt != nil && len(*fs.CustomNumFmt) == 0 { + if style.CustomNumFmt != nil && len(*style.CustomNumFmt) == 0 { err = ErrCustomNumFmt } - return &fs, err + return style, err } -// NewStyle provides a function to create the style for cells by given structure -// pointer or JSON. This function is concurrency safe. Note that -// the 'Font.Color' field uses an RGB color represented in 'RRGGBB' hexadecimal -// notation. +// NewStyle provides a function to create the style for cells by given style +// options. This function is concurrency safe. Note that the 'Font.Color' field +// uses an RGB color represented in 'RRGGBB' hexadecimal notation. // // The following table shows the border types used in 'Border.Type' supported by // excelize: @@ -1983,13 +1972,16 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { // err = f.SetCellStyle("Sheet1", "A6", "A6", style) // // Cell Sheet1!A6 in the Excel Application: martes, 04 de Julio de 2017 -func (f *File) NewStyle(style interface{}) (int, error) { +func (f *File) NewStyle(style *Style) (int, error) { var ( fs *Style font *xlsxFont err error cellXfsID, fontID, borderID, fillID int ) + if style == nil { + return cellXfsID, err + } fs, err = parseFormatStyleSet(style) if err != nil { return cellXfsID, err @@ -2123,9 +2115,8 @@ func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (int, error) { // NewConditionalStyle provides a function to create style for conditional // format by given style format. The parameters are the same with the NewStyle -// function. Note that the color field uses RGB color code and only support to -// set font, fills, alignment and borders currently. -func (f *File) NewConditionalStyle(style string) (int, error) { +// function. +func (f *File) NewConditionalStyle(style *Style) (int, error) { s, err := f.stylesReader() if err != nil { return 0, err @@ -2836,51 +2827,51 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // // Type | Parameters // ---------------+------------------------------------ -// cell | criteria -// | value -// | minimum -// | maximum -// date | criteria -// | value -// | minimum -// | maximum -// time_period | criteria -// text | criteria -// | value -// average | criteria +// cell | Criteria +// | Value +// | Minimum +// | Maximum +// date | Criteria +// | Value +// | Minimum +// | Maximum +// time_period | Criteria +// text | Criteria +// | Value +// average | Criteria // duplicate | (none) // unique | (none) -// top | criteria -// | value -// bottom | criteria -// | value +// top | Criteria +// | Value +// bottom | Criteria +// | Value // blanks | (none) // no_blanks | (none) // errors | (none) // no_errors | (none) -// 2_color_scale | min_type -// | max_type -// | min_value -// | max_value -// | min_color -// | max_color -// 3_color_scale | min_type -// | mid_type -// | max_type -// | min_value -// | mid_value -// | max_value -// | min_color -// | mid_color -// | max_color -// data_bar | min_type -// | max_type -// | min_value -// | max_value -// | bar_color -// formula | criteria -// -// The criteria parameter is used to set the criteria by which the cell data +// 2_color_scale | MinType +// | MaxType +// | MinValue +// | MaxValue +// | MinColor +// | MaxColor +// 3_color_scale | MinType +// | MidType +// | MaxType +// | MinValue +// | MidValue +// | MaxValue +// | MinColor +// | MidColor +// | MaxColor +// data_bar | MinType +// | MaxType +// | MinValue +// | MaxValue +// | BarColor +// formula | Criteria +// +// The 'Criteria' parameter is used to set the criteria by which the cell data // will be evaluated. It has no default value. The most common criteria as // applied to {"type":"cell"} are: // @@ -2902,22 +2893,51 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // value: The value is generally used along with the criteria parameter to set // the rule by which the cell data will be evaluated: // -// f.SetConditionalFormat("Sheet1", "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)) +// err := f.SetConditionalFormat("Sheet1", "D1:D10", +// []excelize.ConditionalFormatOptions{ +// { +// Type: "cell", +// Criteria: ">", +// Format: format, +// Value: "6", +// }, +// }, +// ) // // The value property can also be an cell reference: // -// f.SetConditionalFormat("Sheet1", "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"$C$1"}]`, format)) +// err := f.SetConditionalFormat("Sheet1", "D1:D10", +// []excelize.ConditionalFormatOptions{ +// { +// Type: "cell", +// Criteria: ">", +// Format: format, +// Value: "$C$1", +// }, +// }, +// ) // // type: format - The format parameter is used to specify the format that will // be applied to the cell when the conditional formatting criterion is met. The -// format is created using the NewConditionalStyle() method in the same way as +// format is created using the NewConditionalStyle function in the same way as // cell formats: // -// format, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) +// format, err := f.NewConditionalStyle( +// &excelize.Style{ +// Font: &excelize.Font{Color: "#9A0511"}, +// Fill: excelize.Fill{ +// Type: "pattern", Color: []string{"#FEC7CE"}, Pattern: 1, +// }, +// }, +// ) // if err != nil { // fmt.Println(err) // } -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)) +// err = f.SetConditionalFormat("Sheet1", "D1:D10", +// []excelize.ConditionalFormatOptions{ +// {Type: "cell", Criteria: ">", Format: format, Value: "6"}, +// }, +// ) // // Note: In Excel, a conditional format is superimposed over the existing cell // format and not all cell format properties can be modified. Properties that @@ -2929,19 +2949,50 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // These can be replicated using the following excelize formats: // // // Rose format for bad conditional. -// format1, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) +// format1, err := f.NewConditionalStyle( +// &excelize.Style{ +// Font: &excelize.Font{Color: "#9A0511"}, +// Fill: excelize.Fill{ +// Type: "pattern", Color: []string{"#FEC7CE"}, Pattern: 1, +// }, +// }, +// ) // // // Light yellow format for neutral conditional. -// format2, err = f.NewConditionalStyle(`{"font":{"color":"#9B5713"},"fill":{"type":"pattern","color":["#FEEAA0"],"pattern":1}}`) +// format2, err := f.NewConditionalStyle( +// &excelize.Style{ +// Font: &excelize.Font{Color: "#9B5713"}, +// Fill: excelize.Fill{ +// Type: "pattern", Color: []string{"#FEEAA0"}, Pattern: 1, +// }, +// }, +// ) // // // Light green format for good conditional. -// format3, err = f.NewConditionalStyle(`{"font":{"color":"#09600B"},"fill":{"type":"pattern","color":["#C7EECF"],"pattern":1}}`) +// format3, err := f.NewConditionalStyle( +// &excelize.Style{ +// Font: &excelize.Font{Color: "#09600B"}, +// Fill: excelize.Fill{ +// Type: "pattern", Color: []string{"#C7EECF"}, Pattern: 1, +// }, +// }, +// ) // // type: minimum - The minimum parameter is used to set the lower limiting value // when the criteria is either "between" or "not between". // // // Highlight cells rules: between... -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format)) +// err := f.SetConditionalFormat("Sheet1", "A1:A10", +// []excelize.ConditionalFormatOptions{ +// { +// Type: "cell", +// Criteria: "between", +// Format: format, +// Minimum: "6", +// Maximum: "8", +// }, +// }, +// ) // // type: maximum - The maximum parameter is used to set the upper limiting value // when the criteria is either "between" or "not between". See the previous @@ -2951,98 +3002,184 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // conditional format: // // // Top/Bottom rules: Above Average... -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format1)) +// err := f.SetConditionalFormat("Sheet1", "A1:A10", +// []excelize.ConditionalFormatOptions{ +// { +// Type: "average", +// Criteria: "=", +// Format: format1, +// AboveAverage: true, +// }, +// }, +// ) // // // Top/Bottom rules: Below Average... -// f.SetConditionalFormat("Sheet1", "B1:B10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format2)) +// err := f.SetConditionalFormat("Sheet1", "B1:B10", +// []excelize.ConditionalFormatOptions{ +// { +// Type: "average", +// Criteria: "=", +// Format: format2, +// AboveAverage: false, +// }, +// }, +// ) // // type: duplicate - The duplicate type is used to highlight duplicate cells in a range: // // // Highlight cells rules: Duplicate Values... -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format)) +// err := f.SetConditionalFormat("Sheet1", "A1:A10", +// []excelize.ConditionalFormatOptions{ +// {Type: "duplicate", Criteria: "=", Format: format}, +// }, +// ) // // type: unique - The unique type is used to highlight unique cells in a range: // // // Highlight cells rules: Not Equal To... -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format)) +// err := f.SetConditionalFormat("Sheet1", "A1:A10", +// []excelize.ConditionalFormatOptions{ +// {Type: "unique", Criteria: "=", Format: format}, +// }, +// ) // // type: top - The top type is used to specify the top n values by number or percentage in a range: // // // Top/Bottom rules: Top 10. -// f.SetConditionalFormat("Sheet1", "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6"}]`, format)) +// err := f.SetConditionalFormat("Sheet1", "H1:H10", +// []excelize.ConditionalFormatOptions{ +// { +// Type: "top", +// Criteria: "=", +// Format: format, +// Value: "6", +// }, +// }, +// ) // // The criteria can be used to indicate that a percentage condition is required: // -// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format)) +// err := f.SetConditionalFormat("Sheet1", "A1:A10", +// []excelize.ConditionalFormatOptions{ +// { +// Type: "top", +// Criteria: "=", +// Format: format, +// Value: "6", +// Percent: true, +// }, +// }, +// ) // // type: 2_color_scale - The 2_color_scale type is used to specify Excel's "2 // Color Scale" style conditional format: // // // Color scales: 2 color. -// f.SetConditionalFormat("Sheet1", "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`) +// err := f.SetConditionalFormat("Sheet1", "A1:A10", +// []excelize.ConditionalFormatOptions{ +// { +// Type: "2_color_scale", +// Criteria: "=", +// MinType: "min", +// MaxType: "max", +// MinColor: "#F8696B", +// MaxColor: "#63BE7B", +// }, +// }, +// ) // -// This conditional type can be modified with min_type, max_type, min_value, -// max_value, min_color and max_color, see below. +// This conditional type can be modified with MinType, MaxType, MinValue, +// MaxValue, MinColor and MaxColor, see below. // // type: 3_color_scale - The 3_color_scale type is used to specify Excel's "3 // Color Scale" style conditional format: // // // Color scales: 3 color. -// f.SetConditionalFormat("Sheet1", "A1:A10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`) +// err := f.SetConditionalFormat("Sheet1", "A1:A10", +// []excelize.ConditionalFormatOptions{ +// { +// Type: "3_color_scale", +// Criteria: "=", +// MinType: "min", +// MidType: "percentile", +// MaxType: "max", +// MinColor: "#F8696B", +// MidColor: "#FFEB84", +// MaxColor: "#63BE7B", +// }, +// }, +// ) // -// This conditional type can be modified with min_type, mid_type, max_type, -// min_value, mid_value, max_value, min_color, mid_color and max_color, see +// This conditional type can be modified with MinType, MidType, MaxType, +// MinValue, MidValue, MaxValue, MinColor, MidColor and MaxColor, see // below. // // type: data_bar - The data_bar type is used to specify Excel's "Data Bar" // style conditional format. // -// min_type - The min_type and max_type properties are available when the conditional formatting type is 2_color_scale, 3_color_scale or data_bar. The mid_type is available for 3_color_scale. The properties are used as follows: +// MinType - The MinType and MaxType properties are available when the conditional formatting type is 2_color_scale, 3_color_scale or data_bar. The MidType is available for 3_color_scale. The properties are used as follows: // // // Data Bars: Gradient Fill. -// f.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) +// err := f.SetConditionalFormat("Sheet1", "K1:K10", +// []excelize.ConditionalFormatOptions{ +// { +// Type: "data_bar", +// Criteria: "=", +// MinType: "min", +// MaxType: "max", +// BarColor: "#638EC6", +// }, +// }, +// ) // // The available min/mid/max types are: // -// min (for min_type only) +// min (for MinType only) // num // percent // percentile // formula -// max (for max_type only) +// max (for MaxType only) // -// mid_type - Used for 3_color_scale. Same as min_type, see above. +// MidType - Used for 3_color_scale. Same as MinType, see above. // -// max_type - Same as min_type, see above. +// MaxType - Same as MinType, see above. // -// min_value - The min_value and max_value properties are available when the -// conditional formatting type is 2_color_scale, 3_color_scale or data_bar. The -// mid_value is available for 3_color_scale. +// MinValue - The MinValue and MaxValue properties are available when the +// conditional formatting type is 2_color_scale, 3_color_scale or data_bar. // -// mid_value - Used for 3_color_scale. Same as min_value, see above. +// MidValue - The MidValue is available for 3_color_scale. Same as MinValue, +// see above. // -// max_value - Same as min_value, see above. +// MaxValue - Same as MinValue, see above. // -// min_color - The min_color and max_color properties are available when the +// MinColor - The MinColor and MaxColor properties are available when the // conditional formatting type is 2_color_scale, 3_color_scale or data_bar. -// The mid_color is available for 3_color_scale. The properties are used as -// follows: // -// // Color scales: 3 color. -// f.SetConditionalFormat("Sheet1", "B1:B10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`) +// MidColor - The MidColor is available for 3_color_scale. The properties +// are used as follows: // -// mid_color - Used for 3_color_scale. Same as min_color, see above. +// // Color scales: 3 color. +// err := f.SetConditionalFormat("Sheet1", "B1:B10", +// []excelize.ConditionalFormatOptions{ +// { +// Type: "3_color_scale", +// Criteria: "=", +// MinType: "min", +// MidType: "percentile", +// MaxType: "max", +// MinColor: "#F8696B", +// MidColor: "#FFEB84", +// MaxColor: "#63BE7B", +// }, +// }, +// ) // -// max_color - Same as min_color, see above. +// MaxColor - Same as MinColor, see above. // -// bar_color - Used for data_bar. Same as min_color, see above. -func (f *File) SetConditionalFormat(sheet, reference, opts string) error { - var format []*conditionalOptions - err := json.Unmarshal([]byte(opts), &format) - if err != nil { - return err - } - drawContFmtFunc := map[string]func(p int, ct string, fmtCond *conditionalOptions) *xlsxCfRule{ +// BarColor - Used for data_bar. Same as MinColor, see above. +func (f *File) SetConditionalFormat(sheet, reference string, opts []ConditionalFormatOptions) error { + drawContFmtFunc := map[string]func(p int, ct string, fmtCond *ConditionalFormatOptions) *xlsxCfRule{ "cellIs": drawCondFmtCellIs, "top10": drawCondFmtTop10, "aboveAverage": drawCondFmtAboveAverage, @@ -3059,7 +3196,7 @@ func (f *File) SetConditionalFormat(sheet, reference, opts string) error { return err } var cfRule []*xlsxCfRule - for p, v := range format { + for p, v := range opts { var vt, ct string var ok bool // "type" is a required parameter, check for valid validation types. @@ -3070,7 +3207,7 @@ func (f *File) SetConditionalFormat(sheet, reference, opts string) error { if ok || vt == "expression" { drawFunc, ok := drawContFmtFunc[vt] if ok { - cfRule = append(cfRule, drawFunc(p, ct, v)) + cfRule = append(cfRule, drawFunc(p, ct, &v)) } } } @@ -3086,21 +3223,21 @@ func (f *File) SetConditionalFormat(sheet, reference, opts string) error { // extractCondFmtCellIs provides a function to extract conditional format // settings for cell value (include between, not between, equal, not equal, // greater than and less than) by given conditional formatting rule. -func extractCondFmtCellIs(c *xlsxCfRule) *conditionalOptions { - format := conditionalOptions{Type: "cell", Criteria: operatorType[c.Operator], Format: *c.DxfID} +func extractCondFmtCellIs(c *xlsxCfRule) ConditionalFormatOptions { + format := ConditionalFormatOptions{Type: "cell", Criteria: operatorType[c.Operator], Format: *c.DxfID} if len(c.Formula) == 2 { format.Minimum, format.Maximum = c.Formula[0], c.Formula[1] - return &format + return format } format.Value = c.Formula[0] - return &format + return format } // extractCondFmtTop10 provides a function to extract conditional format // settings for top N (default is top 10) by given conditional formatting // rule. -func extractCondFmtTop10(c *xlsxCfRule) *conditionalOptions { - format := conditionalOptions{ +func extractCondFmtTop10(c *xlsxCfRule) ConditionalFormatOptions { + format := ConditionalFormatOptions{ Type: "top", Criteria: "=", Format: *c.DxfID, @@ -3110,14 +3247,14 @@ func extractCondFmtTop10(c *xlsxCfRule) *conditionalOptions { if c.Bottom { format.Type = "bottom" } - return &format + return format } // extractCondFmtAboveAverage provides a function to extract conditional format // settings for above average and below average by given conditional formatting // rule. -func extractCondFmtAboveAverage(c *xlsxCfRule) *conditionalOptions { - return &conditionalOptions{ +func extractCondFmtAboveAverage(c *xlsxCfRule) ConditionalFormatOptions { + return ConditionalFormatOptions{ Type: "average", Criteria: "=", Format: *c.DxfID, @@ -3128,8 +3265,8 @@ func extractCondFmtAboveAverage(c *xlsxCfRule) *conditionalOptions { // extractCondFmtDuplicateUniqueValues provides a function to extract // conditional format settings for duplicate and unique values by given // conditional formatting rule. -func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule) *conditionalOptions { - return &conditionalOptions{ +func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule) ConditionalFormatOptions { + return ConditionalFormatOptions{ Type: map[string]string{ "duplicateValues": "duplicate", "uniqueValues": "unique", @@ -3142,8 +3279,8 @@ func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule) *conditionalOptions { // extractCondFmtColorScale provides a function to extract conditional format // settings for color scale (include 2 color scale and 3 color scale) by given // conditional formatting rule. -func extractCondFmtColorScale(c *xlsxCfRule) *conditionalOptions { - var format conditionalOptions +func extractCondFmtColorScale(c *xlsxCfRule) ConditionalFormatOptions { + var format ConditionalFormatOptions format.Type, format.Criteria = "2_color_scale", "=" values := len(c.ColorScale.Cfvo) colors := len(c.ColorScale.Color) @@ -3172,35 +3309,35 @@ func extractCondFmtColorScale(c *xlsxCfRule) *conditionalOptions { } format.MaxColor = "#" + strings.TrimPrefix(strings.ToUpper(c.ColorScale.Color[2].RGB), "FF") } - return &format + return format } // extractCondFmtDataBar provides a function to extract conditional format // settings for data bar by given conditional formatting rule. -func extractCondFmtDataBar(c *xlsxCfRule) *conditionalOptions { - format := conditionalOptions{Type: "data_bar", Criteria: "="} +func extractCondFmtDataBar(c *xlsxCfRule) ConditionalFormatOptions { + format := ConditionalFormatOptions{Type: "data_bar", Criteria: "="} if c.DataBar != nil { format.MinType = c.DataBar.Cfvo[0].Type format.MaxType = c.DataBar.Cfvo[1].Type format.BarColor = "#" + strings.TrimPrefix(strings.ToUpper(c.DataBar.Color[0].RGB), "FF") } - return &format + return format } // extractCondFmtExp provides a function to extract conditional format settings // for expression by given conditional formatting rule. -func extractCondFmtExp(c *xlsxCfRule) *conditionalOptions { - format := conditionalOptions{Type: "formula", Format: *c.DxfID} +func extractCondFmtExp(c *xlsxCfRule) ConditionalFormatOptions { + format := ConditionalFormatOptions{Type: "formula", Format: *c.DxfID} if len(c.Formula) > 0 { format.Criteria = c.Formula[0] } - return &format + return format } // GetConditionalFormats returns conditional format settings by given worksheet // name. -func (f *File) GetConditionalFormats(sheet string) (map[string]string, error) { - extractContFmtFunc := map[string]func(c *xlsxCfRule) *conditionalOptions{ +func (f *File) GetConditionalFormats(sheet string) (map[string][]ConditionalFormatOptions, error) { + extractContFmtFunc := map[string]func(c *xlsxCfRule) ConditionalFormatOptions{ "cellIs": extractCondFmtCellIs, "top10": extractCondFmtTop10, "aboveAverage": extractCondFmtAboveAverage, @@ -3211,20 +3348,19 @@ func (f *File) GetConditionalFormats(sheet string) (map[string]string, error) { "expression": extractCondFmtExp, } - conditionalFormats := make(map[string]string) + conditionalFormats := make(map[string][]ConditionalFormatOptions) ws, err := f.workSheetReader(sheet) if err != nil { return conditionalFormats, err } for _, cf := range ws.ConditionalFormatting { - var opts []*conditionalOptions + var opts []ConditionalFormatOptions for _, cr := range cf.CfRule { if extractFunc, ok := extractContFmtFunc[cr.Type]; ok { opts = append(opts, extractFunc(cr)) } } - options, _ := json.Marshal(opts) - conditionalFormats[cf.SQRef] = string(options) + conditionalFormats[cf.SQRef] = opts } return conditionalFormats, err } @@ -3248,7 +3384,7 @@ func (f *File) UnsetConditionalFormat(sheet, reference string) error { // drawCondFmtCellIs provides a function to create conditional formatting rule // for cell value (include between, not between, equal, not equal, greater // than and less than) by given priority, criteria type and format settings. -func drawCondFmtCellIs(p int, ct string, format *conditionalOptions) *xlsxCfRule { +func drawCondFmtCellIs(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { c := &xlsxCfRule{ Priority: p + 1, Type: validType[format.Type], @@ -3268,7 +3404,7 @@ func drawCondFmtCellIs(p int, ct string, format *conditionalOptions) *xlsxCfRule // drawCondFmtTop10 provides a function to create conditional formatting rule // for top N (default is top 10) by given priority, criteria type and format // settings. -func drawCondFmtTop10(p int, ct string, format *conditionalOptions) *xlsxCfRule { +func drawCondFmtTop10(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { c := &xlsxCfRule{ Priority: p + 1, Bottom: format.Type == "bottom", @@ -3286,7 +3422,7 @@ func drawCondFmtTop10(p int, ct string, format *conditionalOptions) *xlsxCfRule // drawCondFmtAboveAverage provides a function to create conditional // formatting rule for above average and below average by given priority, // criteria type and format settings. -func drawCondFmtAboveAverage(p int, ct string, format *conditionalOptions) *xlsxCfRule { +func drawCondFmtAboveAverage(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { return &xlsxCfRule{ Priority: p + 1, Type: validType[format.Type], @@ -3298,7 +3434,7 @@ func drawCondFmtAboveAverage(p int, ct string, format *conditionalOptions) *xlsx // drawCondFmtDuplicateUniqueValues provides a function to create conditional // formatting rule for duplicate and unique values by given priority, criteria // type and format settings. -func drawCondFmtDuplicateUniqueValues(p int, ct string, format *conditionalOptions) *xlsxCfRule { +func drawCondFmtDuplicateUniqueValues(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { return &xlsxCfRule{ Priority: p + 1, Type: validType[format.Type], @@ -3309,7 +3445,7 @@ func drawCondFmtDuplicateUniqueValues(p int, ct string, format *conditionalOptio // drawCondFmtColorScale provides a function to create conditional formatting // rule for color scale (include 2 color scale and 3 color scale) by given // priority, criteria type and format settings. -func drawCondFmtColorScale(p int, ct string, format *conditionalOptions) *xlsxCfRule { +func drawCondFmtColorScale(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { minValue := format.MinValue if minValue == "" { minValue = "0" @@ -3346,7 +3482,7 @@ func drawCondFmtColorScale(p int, ct string, format *conditionalOptions) *xlsxCf // drawCondFmtDataBar provides a function to create conditional formatting // rule for data bar by given priority, criteria type and format settings. -func drawCondFmtDataBar(p int, ct string, format *conditionalOptions) *xlsxCfRule { +func drawCondFmtDataBar(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { return &xlsxCfRule{ Priority: p + 1, Type: validType[format.Type], @@ -3359,7 +3495,7 @@ func drawCondFmtDataBar(p int, ct string, format *conditionalOptions) *xlsxCfRul // drawCondFmtExp provides a function to create conditional formatting rule // for expression by given priority, criteria type and format settings. -func drawCondFmtExp(p int, ct string, format *conditionalOptions) *xlsxCfRule { +func drawCondFmtExp(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { return &xlsxCfRule{ Priority: p + 1, Type: validType[format.Type], diff --git a/styles_test.go b/styles_test.go index 0d216b0b11..44ba535d6d 100644 --- a/styles_test.go +++ b/styles_test.go @@ -1,7 +1,6 @@ package excelize import ( - "fmt" "math" "path/filepath" "strings" @@ -13,15 +12,15 @@ import ( func TestStyleFill(t *testing.T) { cases := []struct { label string - format string + format *Style expectFill bool }{{ label: "no_fill", - format: `{"alignment":{"wrap_text":true}}`, + format: &Style{Alignment: &Alignment{WrapText: true}}, expectFill: false, }, { label: "fill", - format: `{"fill":{"type":"pattern","pattern":1,"color":["#000000"]}}`, + format: &Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"#000000"}}}, expectFill: true, }} @@ -40,9 +39,9 @@ func TestStyleFill(t *testing.T) { } } f := NewFile() - styleID1, err := f.NewStyle(`{"fill":{"type":"pattern","pattern":1,"color":["#000000"]}}`) + styleID1, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"#000000"}}}) assert.NoError(t, err) - styleID2, err := f.NewStyle(`{"fill":{"type":"pattern","pattern":1,"color":["#000000"]}}`) + styleID2, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"#000000"}}}) assert.NoError(t, err) assert.Equal(t, styleID1, styleID2) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleFill.xlsx"))) @@ -51,23 +50,23 @@ func TestStyleFill(t *testing.T) { func TestSetConditionalFormat(t *testing.T) { cases := []struct { label string - format string + format []ConditionalFormatOptions rules []*xlsxCfRule }{{ label: "3_color_scale", - format: `[{ - "type":"3_color_scale", - "criteria":"=", - "min_type":"num", - "mid_type":"num", - "max_type":"num", - "min_value": "-10", - "mid_value": "0", - "max_value": "10", - "min_color":"ff0000", - "mid_color":"00ff00", - "max_color":"0000ff" - }]`, + format: []ConditionalFormatOptions{{ + Type: "3_color_scale", + Criteria: "=", + MinType: "num", + MidType: "num", + MaxType: "num", + MinValue: "-10", + MidValue: "0", + MaxValue: "10", + MinColor: "ff0000", + MidColor: "00ff00", + MaxColor: "0000ff", + }}, rules: []*xlsxCfRule{{ Priority: 1, Type: "colorScale", @@ -93,16 +92,16 @@ func TestSetConditionalFormat(t *testing.T) { }}, }, { label: "3_color_scale default min/mid/max", - format: `[{ - "type":"3_color_scale", - "criteria":"=", - "min_type":"num", - "mid_type":"num", - "max_type":"num", - "min_color":"ff0000", - "mid_color":"00ff00", - "max_color":"0000ff" - }]`, + format: []ConditionalFormatOptions{{ + Type: "3_color_scale", + Criteria: "=", + MinType: "num", + MidType: "num", + MaxType: "num", + MinColor: "ff0000", + MidColor: "00ff00", + MaxColor: "0000ff", + }}, rules: []*xlsxCfRule{{ Priority: 1, Type: "colorScale", @@ -128,14 +127,14 @@ func TestSetConditionalFormat(t *testing.T) { }}, }, { label: "2_color_scale default min/max", - format: `[{ - "type":"2_color_scale", - "criteria":"=", - "min_type":"num", - "max_type":"num", - "min_color":"ff0000", - "max_color":"0000ff" - }]`, + format: []ConditionalFormatOptions{{ + Type: "2_color_scale", + Criteria: "=", + MinType: "num", + MaxType: "num", + MinColor: "ff0000", + MaxColor: "0000ff", + }}, rules: []*xlsxCfRule{{ Priority: 1, Type: "colorScale", @@ -177,18 +176,18 @@ func TestSetConditionalFormat(t *testing.T) { } func TestGetConditionalFormats(t *testing.T) { - for _, format := range []string{ - `[{"type":"cell","format":1,"criteria":"greater than","value":"6"}]`, - `[{"type":"cell","format":1,"criteria":"between","minimum":"6","maximum":"8"}]`, - `[{"type":"top","format":1,"criteria":"=","value":"6"}]`, - `[{"type":"bottom","format":1,"criteria":"=","value":"6"}]`, - `[{"type":"average","above_average":true,"format":1,"criteria":"="}]`, - `[{"type":"duplicate","format":1,"criteria":"="}]`, - `[{"type":"unique","format":1,"criteria":"="}]`, - `[{"type":"3_color_scale","criteria":"=","min_type":"num","mid_type":"num","max_type":"num","min_value":"-10","mid_value":"50","max_value":"10","min_color":"#FF0000","mid_color":"#00FF00","max_color":"#0000FF"}]`, - `[{"type":"2_color_scale","criteria":"=","min_type":"num","max_type":"num","min_color":"#FF0000","max_color":"#0000FF"}]`, - `[{"type":"data_bar","criteria":"=","min_type":"min","max_type":"max","bar_color":"#638EC6"}]`, - `[{"type":"formula","format":1,"criteria":"="}]`, + for _, format := range [][]ConditionalFormatOptions{ + {{Type: "cell", Format: 1, Criteria: "greater than", Value: "6"}}, + {{Type: "cell", Format: 1, Criteria: "between", Minimum: "6", Maximum: "8"}}, + {{Type: "top", Format: 1, Criteria: "=", Value: "6"}}, + {{Type: "bottom", Format: 1, Criteria: "=", Value: "6"}}, + {{Type: "average", AboveAverage: true, Format: 1, Criteria: "="}}, + {{Type: "duplicate", Format: 1, Criteria: "="}}, + {{Type: "unique", Format: 1, Criteria: "="}}, + {{Type: "3_color_scale", Criteria: "=", MinType: "num", MidType: "num", MaxType: "num", MinValue: "-10", MidValue: "50", MaxValue: "10", MinColor: "#FF0000", MidColor: "#00FF00", MaxColor: "#0000FF"}}, + {{Type: "2_color_scale", Criteria: "=", MinType: "num", MaxType: "num", MinColor: "#FF0000", MaxColor: "#0000FF"}}, + {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarColor: "#638EC6"}}, + {{Type: "formula", Format: 1, Criteria: "="}}, } { f := NewFile() err := f.SetConditionalFormat("Sheet1", "A1:A2", format) @@ -210,9 +209,9 @@ func TestUnsetConditionalFormat(t *testing.T) { f := NewFile() assert.NoError(t, f.SetCellValue("Sheet1", "A1", 7)) assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10")) - format, err := f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) + format, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "#9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"#FEC7CE"}, Pattern: 1}}) assert.NoError(t, err) - assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format))) + assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", []ConditionalFormatOptions{{Type: "cell", Criteria: ">", Format: format, Value: "6"}})) assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10")) // Test unset conditional format on not exists worksheet assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN does not exist") @@ -224,7 +223,7 @@ func TestUnsetConditionalFormat(t *testing.T) { func TestNewStyle(t *testing.T) { f := NewFile() - styleID, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) + styleID, err := f.NewStyle(&Style{Font: &Font{Bold: true, Italic: true, Family: "Times New Roman", Size: 36, Color: "#777777"}}) assert.NoError(t, err) styles, err := f.stylesReader() assert.NoError(t, err) @@ -234,8 +233,8 @@ func TestNewStyle(t *testing.T) { assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") _, err = f.NewStyle(&Style{}) assert.NoError(t, err) - _, err = f.NewStyle(Style{}) - assert.EqualError(t, err, ErrParameterInvalid.Error()) + _, err = f.NewStyle(nil) + assert.NoError(t, err) var exp string _, err = f.NewStyle(&Style{CustomNumFmt: &exp}) @@ -326,7 +325,7 @@ func TestNewConditionalStyle(t *testing.T) { // Test create conditional style with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) - _, err := f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) + _, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "#9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"#FEC7CE"}, Pattern: 1}}) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } @@ -378,13 +377,13 @@ func TestThemeReader(t *testing.T) { func TestSetCellStyle(t *testing.T) { f := NewFile() - // Test set cell style on not exists worksheet. + // Test set cell style on not exists worksheet assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN does not exist") - // Test set cell style with invalid style ID. + // Test set cell style with invalid style ID assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", -1), newInvalidStyleID(-1).Error()) - // Test set cell style with not exists style ID. + // Test set cell style with not exists style ID assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", 10), newInvalidStyleID(10).Error()) - // Test set cell style with unsupported charset style sheet. + // Test set cell style with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", 1), "XML syntax error on line 1: invalid UTF-8") @@ -395,7 +394,7 @@ func TestGetStyleID(t *testing.T) { styleID, err := f.getStyleID(&xlsxStyleSheet{}, nil) assert.NoError(t, err) assert.Equal(t, -1, styleID) - // Test get style ID with unsupported charset style sheet. + // Test get style ID with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) _, err = f.getStyleID(&xlsxStyleSheet{ @@ -429,11 +428,11 @@ func TestThemeColor(t *testing.T) { func TestGetNumFmtID(t *testing.T) { f := NewFile() - fs1, err := parseFormatStyleSet(`{"protection":{"hidden":false,"locked":false},"number_format":10}`) + fs1, err := parseFormatStyleSet(&Style{Protection: &Protection{Hidden: false, Locked: false}, NumFmt: 10}) assert.NoError(t, err) id1 := getNumFmtID(&xlsxStyleSheet{}, fs1) - fs2, err := parseFormatStyleSet(`{"protection":{"hidden":false,"locked":false},"number_format":0}`) + fs2, err := parseFormatStyleSet(&Style{Protection: &Protection{Hidden: false, Locked: false}, NumFmt: 0}) assert.NoError(t, err) id2 := getNumFmtID(&xlsxStyleSheet{}, fs2) diff --git a/table.go b/table.go index 90cc97fd5a..42aa35a69f 100644 --- a/table.go +++ b/table.go @@ -12,7 +12,6 @@ package excelize import ( - "encoding/json" "encoding/xml" "fmt" "regexp" @@ -22,64 +21,54 @@ import ( // parseTableOptions provides a function to parse the format settings of the // table with default value. -func parseTableOptions(opts string) (*tableOptions, error) { - options := tableOptions{ShowRowStripes: true} - err := json.Unmarshal(fallbackOptions(opts), &options) - return &options, err +func parseTableOptions(opts *TableOptions) *TableOptions { + if opts == nil { + return &TableOptions{ShowRowStripes: boolPtr(true)} + } + if opts.ShowRowStripes == nil { + opts.ShowRowStripes = boolPtr(true) + } + return opts } // AddTable provides the method to add table in a worksheet by given worksheet // name, range reference and format set. For example, create a table of A1:D5 // on Sheet1: // -// err := f.AddTable("Sheet1", "A1", "D5", "") +// err := f.AddTable("Sheet1", "A1:D5", nil) // // Create a table of F2:H6 on Sheet2 with format set: // -// err := f.AddTable("Sheet2", "F2", "H6", `{ -// "table_name": "table", -// "table_style": "TableStyleMedium2", -// "show_first_column": true, -// "show_last_column": true, -// "show_row_stripes": false, -// "show_column_stripes": true -// }`) +// err := f.AddTable("Sheet2", "F2:H6", &excelize.TableOptions{ +// Name: "table", +// StyleName: "TableStyleMedium2", +// ShowFirstColumn: true, +// ShowLastColumn: true, +// ShowRowStripes: &disable, +// ShowColumnStripes: true, +// }) // // Note that the table must be at least two lines including the header. The // header cells must contain strings and must be unique, and must set the // header row data of the table before calling the AddTable function. Multiple // tables range reference that can't have an intersection. // -// table_name: The name of the table, in the same worksheet name of the table should be unique +// Name: The name of the table, in the same worksheet name of the table should be unique // -// table_style: The built-in table style names +// StyleName: The built-in table style names // // TableStyleLight1 - TableStyleLight21 // TableStyleMedium1 - TableStyleMedium28 // TableStyleDark1 - TableStyleDark11 -func (f *File) AddTable(sheet, hCell, vCell, opts string) error { - options, err := parseTableOptions(opts) - if err != nil { - return err - } +func (f *File) AddTable(sheet, reference string, opts *TableOptions) error { + options := parseTableOptions(opts) // Coordinate conversion, convert C1:B3 to 2,0,1,2. - hCol, hRow, err := CellNameToCoordinates(hCell) - if err != nil { - return err - } - vCol, vRow, err := CellNameToCoordinates(vCell) + coordinates, err := rangeRefToCoordinates(reference) if err != nil { return err } - - if vCol < hCol { - vCol, hCol = hCol, vCol - } - - if vRow < hRow { - vRow, hRow = hRow, vRow - } - + // Correct table reference range, such correct C1:B3 to B1:C3. + _ = sortCoordinates(coordinates) tableID := f.countTables() + 1 sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml" tableXML := strings.ReplaceAll(sheetRelationshipsTableXML, "..", "xl") @@ -91,7 +80,7 @@ func (f *File) AddTable(sheet, hCell, vCell, opts string) error { return err } f.addSheetNameSpace(sheet, SourceRelationship) - if err = f.addTable(sheet, tableXML, hCol, hRow, vCol, vRow, tableID, options); err != nil { + if err = f.addTable(sheet, tableXML, coordinates[0], coordinates[1], coordinates[2], coordinates[3], tableID, options); err != nil { return err } return f.addContentTypePart(tableID, "table") @@ -159,7 +148,7 @@ func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn, // addTable provides a function to add table by given worksheet name, // range reference and format set. -func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *tableOptions) error { +func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *TableOptions) error { // Correct the minimum number of rows, the table at least two lines. if y1 == y2 { y2++ @@ -171,7 +160,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *tab return err } tableColumns, _ := f.setTableHeader(sheet, x1, y1, x2) - name := opts.TableName + name := opts.Name if name == "" { name = "Table" + strconv.Itoa(i) } @@ -189,10 +178,10 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *tab TableColumn: tableColumns, }, TableStyleInfo: &xlsxTableStyleInfo{ - Name: opts.TableStyle, + Name: opts.StyleName, ShowFirstColumn: opts.ShowFirstColumn, ShowLastColumn: opts.ShowLastColumn, - ShowRowStripes: opts.ShowRowStripes, + ShowRowStripes: *opts.ShowRowStripes, ShowColumnStripes: opts.ShowColumnStripes, }, } @@ -201,36 +190,30 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *tab return nil } -// parseAutoFilterOptions provides a function to parse the settings of the auto -// filter. -func parseAutoFilterOptions(opts string) (*autoFilterOptions, error) { - options := autoFilterOptions{} - err := json.Unmarshal([]byte(opts), &options) - return &options, err -} - // AutoFilter provides the method to add auto filter in a worksheet by given // worksheet name, range reference and settings. An auto filter in Excel is a // way of filtering a 2D range of data based on some simple criteria. For // example applying an auto filter to a cell range A1:D4 in the Sheet1: // -// err := f.AutoFilter("Sheet1", "A1", "D4", "") +// err := f.AutoFilter("Sheet1", "A1:D4", nil) // // Filter data in an auto filter: // -// err := f.AutoFilter("Sheet1", "A1", "D4", `{"column":"B","expression":"x != blanks"}`) +// err := f.AutoFilter("Sheet1", "A1:D4", &excelize.AutoFilterOptions{ +// Column: "B", Expression: "x != blanks", +// }) // -// column defines the filter columns in an auto filter range based on simple +// Column defines the filter columns in an auto filter range based on simple // criteria // // It isn't sufficient to just specify the filter condition. You must also // hide any rows that don't match the filter condition. Rows are hidden using -// the SetRowVisible() method. Excelize can't filter rows automatically since +// the SetRowVisible function. Excelize can't filter rows automatically since // this isn't part of the file format. // // Setting a filter criteria for a column: // -// expression defines the conditions, the following operators are available +// Expression defines the conditions, the following operators are available // for setting the filter criteria: // // == @@ -278,28 +261,15 @@ func parseAutoFilterOptions(opts string) (*autoFilterOptions, error) { // x < 2000 // col < 2000 // Price < 2000 -func (f *File) AutoFilter(sheet, hCell, vCell, opts string) error { - hCol, hRow, err := CellNameToCoordinates(hCell) - if err != nil { - return err - } - vCol, vRow, err := CellNameToCoordinates(vCell) +func (f *File) AutoFilter(sheet, reference string, opts *AutoFilterOptions) error { + coordinates, err := rangeRefToCoordinates(reference) if err != nil { return err } - - if vCol < hCol { - vCol, hCol = hCol, vCol - } - - if vRow < hRow { - vRow, hRow = hRow, vRow - } - - options, _ := parseAutoFilterOptions(opts) - cellStart, _ := CoordinatesToCellName(hCol, hRow, true) - cellEnd, _ := CoordinatesToCellName(vCol, vRow, true) - ref, filterDB := cellStart+":"+cellEnd, "_xlnm._FilterDatabase" + _ = sortCoordinates(coordinates) + // Correct reference range, such correct C1:B3 to B1:C3. + ref, _ := f.coordinatesToRangeRef(coordinates, true) + filterDB := "_xlnm._FilterDatabase" wb, err := f.workbookReader() if err != nil { return err @@ -332,13 +302,13 @@ func (f *File) AutoFilter(sheet, hCell, vCell, opts string) error { wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) } } - refRange := vCol - hCol - return f.autoFilter(sheet, ref, refRange, hCol, options) + refRange := coordinates[2] - coordinates[0] + return f.autoFilter(sheet, ref, refRange, coordinates[0], opts) } // autoFilter provides a function to extract the tokens from the filter // expression. The tokens are mainly non-whitespace groups. -func (f *File) autoFilter(sheet, ref string, refRange, col int, opts *autoFilterOptions) error { +func (f *File) autoFilter(sheet, ref string, refRange, col int, opts *AutoFilterOptions) error { ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -351,7 +321,7 @@ func (f *File) autoFilter(sheet, ref string, refRange, col int, opts *autoFilter Ref: ref, } ws.AutoFilter = filter - if opts.Column == "" || opts.Expression == "" { + if opts == nil || opts.Column == "" || opts.Expression == "" { return nil } diff --git a/table_test.go b/table_test.go index d26d20ca9a..1e1afae372 100644 --- a/table_test.go +++ b/table_test.go @@ -11,22 +11,28 @@ import ( func TestAddTable(t *testing.T) { f, err := prepareTestBook1() assert.NoError(t, err) - assert.NoError(t, f.AddTable("Sheet1", "B26", "A21", `{}`)) - assert.NoError(t, f.AddTable("Sheet2", "A2", "B5", `{"table_name":"table","table_style":"TableStyleMedium2", "show_first_column":true,"show_last_column":true,"show_row_stripes":false,"show_column_stripes":true}`)) - assert.NoError(t, f.AddTable("Sheet2", "F1", "F1", `{"table_style":"TableStyleMedium8"}`)) + assert.NoError(t, f.AddTable("Sheet1", "B26:A21", nil)) + assert.NoError(t, f.AddTable("Sheet2", "A2:B5", &TableOptions{ + Name: "table", + StyleName: "TableStyleMedium2", + ShowFirstColumn: true, + ShowLastColumn: true, + ShowRowStripes: boolPtr(true), + ShowColumnStripes: true, + }, + )) + assert.NoError(t, f.AddTable("Sheet2", "F1:F1", &TableOptions{StyleName: "TableStyleMedium8"})) // Test add table in not exist worksheet - assert.EqualError(t, f.AddTable("SheetN", "B26", "A21", `{}`), "sheet SheetN does not exist") - // Test add table with illegal options - assert.EqualError(t, f.AddTable("Sheet1", "B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") + assert.EqualError(t, f.AddTable("SheetN", "B26:A21", nil), "sheet SheetN does not exist") // Test add table with illegal cell reference - assert.EqualError(t, f.AddTable("Sheet1", "A", "B1", `{}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, f.AddTable("Sheet1", "A1", "B", `{}`), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + assert.EqualError(t, f.AddTable("Sheet1", "A:B1", nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.AddTable("Sheet1", "A1:B", nil), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) // Test add table with invalid sheet name - assert.EqualError(t, f.AddTable("Sheet:1", "B26", "A21", `{}`), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddTable("Sheet:1", "B26:A21", nil), ErrSheetNameInvalid.Error()) // Test addTable with illegal cell reference f = NewFile() assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell reference [0, 0]") @@ -43,73 +49,66 @@ func TestAutoFilter(t *testing.T) { outFile := filepath.Join("test", "TestAutoFilter%d.xlsx") f, err := prepareTestBook1() assert.NoError(t, err) - formats := []string{ - ``, - `{"column":"B","expression":"x != blanks"}`, - `{"column":"B","expression":"x == blanks"}`, - `{"column":"B","expression":"x != nonblanks"}`, - `{"column":"B","expression":"x == nonblanks"}`, - `{"column":"B","expression":"x <= 1 and x >= 2"}`, - `{"column":"B","expression":"x == 1 or x == 2"}`, - `{"column":"B","expression":"x == 1 or x == 2*"}`, - } - for i, format := range formats { + for i, opts := range []*AutoFilterOptions{ + nil, + {Column: "B", Expression: ""}, + {Column: "B", Expression: "x != blanks"}, + {Column: "B", Expression: "x == blanks"}, + {Column: "B", Expression: "x != nonblanks"}, + {Column: "B", Expression: "x == nonblanks"}, + {Column: "B", Expression: "x <= 1 and x >= 2"}, + {Column: "B", Expression: "x == 1 or x == 2"}, + {Column: "B", Expression: "x == 1 or x == 2*"}, + } { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { - err = f.AutoFilter("Sheet1", "D4", "B1", format) - assert.NoError(t, err) + assert.NoError(t, f.AutoFilter("Sheet1", "D4:B1", opts)) assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) }) } // Test add auto filter with invalid sheet name - assert.EqualError(t, f.AutoFilter("Sheet:1", "A1", "B1", ""), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AutoFilter("Sheet:1", "A1:B1", nil), ErrSheetNameInvalid.Error()) // Test add auto filter with illegal cell reference - assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + assert.EqualError(t, f.AutoFilter("Sheet1", "A:B1", nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.AutoFilter("Sheet1", "A1:B", nil), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) // Test add auto filter with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) - assert.EqualError(t, f.AutoFilter("Sheet1", "D4", "B1", formats[0]), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AutoFilter("Sheet1", "D4:B1", nil), "XML syntax error on line 1: invalid UTF-8") } func TestAutoFilterError(t *testing.T) { outFile := filepath.Join("test", "TestAutoFilterError%d.xlsx") - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - formats := []string{ - `{"column":"B","expression":"x <= 1 and x >= blanks"}`, - `{"column":"B","expression":"x -- y or x == *2*"}`, - `{"column":"B","expression":"x != y or x ? *2"}`, - `{"column":"B","expression":"x -- y o r x == *2"}`, - `{"column":"B","expression":"x -- y"}`, - `{"column":"A","expression":"x -- y"}`, - } - for i, format := range formats { + assert.NoError(t, err) + for i, opts := range []*AutoFilterOptions{ + {Column: "B", Expression: "x <= 1 and x >= blanks"}, + {Column: "B", Expression: "x -- y or x == *2*"}, + {Column: "B", Expression: "x != y or x ? *2"}, + {Column: "B", Expression: "x -- y o r x == *2"}, + {Column: "B", Expression: "x -- y"}, + {Column: "A", Expression: "x -- y"}, + } { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { - err = f.AutoFilter("Sheet2", "D4", "B1", format) - if assert.Error(t, err) { + if assert.Error(t, f.AutoFilter("Sheet2", "D4:B1", opts)) { assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) } }) } - assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, &autoFilterOptions{ + assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, &AutoFilterOptions{ Column: "A", Expression: "", }), "sheet SheetN does not exist") - assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &autoFilterOptions{ + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &AutoFilterOptions{ Column: "-", Expression: "-", }), newInvalidColumnNameError("-").Error()) - assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, &autoFilterOptions{ + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, &AutoFilterOptions{ Column: "A", Expression: "-", }), `incorrect index of column 'A'`) - assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &autoFilterOptions{ + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &AutoFilterOptions{ Column: "A", Expression: "-", }), `incorrect number of tokens in criteria '-'`) diff --git a/workbook.go b/workbook.go index 1367eac82f..b3ee7ffafa 100644 --- a/workbook.go +++ b/workbook.go @@ -59,8 +59,18 @@ func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error) { return opts, err } -// ProtectWorkbook provides a function to prevent other users from accidentally or -// deliberately changing, moving, or deleting data in a workbook. +// ProtectWorkbook provides a function to prevent other users from viewing +// hidden worksheets, adding, moving, deleting, or hiding worksheets, and +// renaming worksheets in a workbook. The optional field AlgorithmName +// specified hash algorithm, support XOR, MD4, MD5, SHA-1, SHA2-56, SHA-384, +// and SHA-512 currently, if no hash algorithm specified, will be using the XOR +// algorithm as default. The generated workbook only works on Microsoft Office +// 2007 and later. For example, protect workbook with protection settings: +// +// err := f.ProtectWorkbook(&excelize.WorkbookProtectionOptions{ +// Password: "password", +// LockStructure: true, +// }) func (f *File) ProtectWorkbook(opts *WorkbookProtectionOptions) error { wb, err := f.workbookReader() if err != nil { @@ -93,8 +103,8 @@ func (f *File) ProtectWorkbook(opts *WorkbookProtectionOptions) error { } // UnprotectWorkbook provides a function to remove protection for workbook, -// specified the second optional password parameter to remove workbook -// protection with password verification. +// specified the optional password parameter to remove workbook protection with +// password verification. func (f *File) UnprotectWorkbook(password ...string) error { wb, err := f.workbookReader() if err != nil { diff --git a/workbook_test.go b/workbook_test.go index a3b2b52672..67cf5c81c4 100644 --- a/workbook_test.go +++ b/workbook_test.go @@ -21,11 +21,11 @@ func TestWorkbookProps(t *testing.T) { opts, err := f.GetWorkbookProps() assert.NoError(t, err) assert.Equal(t, expected, opts) - // Test set workbook properties with unsupported charset workbook. + // Test set workbook properties with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.SetWorkbookProps(&expected), "XML syntax error on line 1: invalid UTF-8") - // Test get workbook properties with unsupported charset workbook. + // Test get workbook properties with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) _, err = f.GetWorkbookProps() diff --git a/xmlApp.go b/xmlApp.go index f21e5f952f..abfd82b3d7 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -15,13 +15,13 @@ import "encoding/xml" // AppProperties directly maps the document application properties. type AppProperties struct { - Application string `json:"application"` - ScaleCrop bool `json:"scale_crop"` - DocSecurity int `json:"doc_security"` - Company string `json:"company"` - LinksUpToDate bool `json:"links_up_to_date"` - HyperlinksChanged bool `json:"hyperlinks_changed"` - AppVersion string `json:"app_version"` + Application string + ScaleCrop bool + DocSecurity int + Company string + LinksUpToDate bool + HyperlinksChanged bool + AppVersion string } // xlsxProperties specifies to an OOXML document properties such as the diff --git a/xmlChart.go b/xmlChart.go index 5165ea09ed..10e6c2e3cd 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -518,136 +518,83 @@ type cPageMargins struct { T float64 `xml:"t,attr"` } -// chartAxisOptions directly maps the format settings of the chart axis. -type chartAxisOptions struct { - None bool `json:"none"` - Crossing string `json:"crossing"` - MajorGridlines bool `json:"major_grid_lines"` - MinorGridlines bool `json:"minor_grid_lines"` - MajorTickMark string `json:"major_tick_mark"` - MinorTickMark string `json:"minor_tick_mark"` - MinorUnitType string `json:"minor_unit_type"` - MajorUnit float64 `json:"major_unit"` - MajorUnitType string `json:"major_unit_type"` - TickLabelSkip int `json:"tick_label_skip"` - DisplayUnits string `json:"display_units"` - DisplayUnitsVisible bool `json:"display_units_visible"` - DateAxis bool `json:"date_axis"` - ReverseOrder bool `json:"reverse_order"` - Maximum *float64 `json:"maximum"` - Minimum *float64 `json:"minimum"` - NumFormat string `json:"number_format"` - Font Font `json:"font"` - LogBase float64 `json:"logbase"` - NameLayout layoutOptions `json:"name_layout"` -} - -// chartDimensionOptions directly maps the dimension of the chart. -type chartDimensionOptions struct { - Width int `json:"width"` - Height int `json:"height"` -} - -// chartOptions directly maps the format settings of the chart. -type chartOptions struct { - Type string `json:"type"` - Series []chartSeriesOptions `json:"series"` - Format pictureOptions `json:"format"` - Dimension chartDimensionOptions `json:"dimension"` - Legend chartLegendOptions `json:"legend"` - Title chartTitleOptions `json:"title"` - VaryColors bool `json:"vary_colors"` - XAxis chartAxisOptions `json:"x_axis"` - YAxis chartAxisOptions `json:"y_axis"` - Chartarea struct { - Border struct { - None bool `json:"none"` - } `json:"border"` - Fill struct { - Color string `json:"color"` - } `json:"fill"` - Pattern struct { - Pattern string `json:"pattern"` - FgColor string `json:"fg_color"` - BgColor string `json:"bg_color"` - } `json:"pattern"` - } `json:"chartarea"` - Plotarea struct { - ShowBubbleSize bool `json:"show_bubble_size"` - ShowCatName bool `json:"show_cat_name"` - ShowLeaderLines bool `json:"show_leader_lines"` - ShowPercent bool `json:"show_percent"` - ShowSerName bool `json:"show_series_name"` - ShowVal bool `json:"show_val"` - Gradient struct { - Colors []string `json:"colors"` - } `json:"gradient"` - Border struct { - Color string `json:"color"` - Width int `json:"width"` - DashType string `json:"dash_type"` - } `json:"border"` - Fill struct { - Color string `json:"color"` - } `json:"fill"` - Layout layoutOptions `json:"layout"` - } `json:"plotarea"` - ShowBlanksAs string `json:"show_blanks_as"` - ShowHiddenData bool `json:"show_hidden_data"` - SetRotation int `json:"set_rotation"` - HoleSize int `json:"hole_size"` - order int -} - -// chartLegendOptions directly maps the format settings of the chart legend. -type chartLegendOptions struct { - None bool `json:"none"` - DeleteSeries []int `json:"delete_series"` - Font Font `json:"font"` - Layout layoutOptions `json:"layout"` - Position string `json:"position"` - ShowLegendEntry bool `json:"show_legend_entry"` - ShowLegendKey bool `json:"show_legend_key"` -} - -// chartSeriesOptions directly maps the format settings of the chart series. -type chartSeriesOptions struct { - Name string `json:"name"` - Categories string `json:"categories"` - Values string `json:"values"` - Line struct { - None bool `json:"none"` - Color string `json:"color"` - Smooth bool `json:"smooth"` - Width float64 `json:"width"` - } `json:"line"` - Marker struct { - Symbol string `json:"symbol"` - Size int `json:"size"` - Width float64 `json:"width"` - Border struct { - Color string `json:"color"` - None bool `json:"none"` - } `json:"border"` - Fill struct { - Color string `json:"color"` - None bool `json:"none"` - } `json:"fill"` - } `json:"marker"` -} - -// chartTitleOptions directly maps the format settings of the chart title. -type chartTitleOptions struct { - None bool `json:"none"` - Name string `json:"name"` - Overlay bool `json:"overlay"` - Layout layoutOptions `json:"layout"` -} - -// layoutOptions directly maps the format settings of the element layout. -type layoutOptions struct { - X float64 `json:"x"` - Y float64 `json:"y"` - Width float64 `json:"width"` - Height float64 `json:"height"` +// ChartAxis directly maps the format settings of the chart axis. +type ChartAxis struct { + None bool + MajorGridLines bool + MinorGridLines bool + MajorUnit float64 + TickLabelSkip int + ReverseOrder bool + Maximum *float64 + Minimum *float64 + Font Font + LogBase float64 +} + +// ChartDimension directly maps the dimension of the chart. +type ChartDimension struct { + Width *int + Height *int +} + +// ChartPlotArea directly maps the format settings of the plot area. +type ChartPlotArea struct { + ShowBubbleSize bool + ShowCatName bool + ShowLeaderLines bool + ShowPercent bool + ShowSerName bool + ShowVal bool +} + +// Chart directly maps the format settings of the chart. +type Chart struct { + Type string + Series []ChartSeries + Format PictureOptions + Dimension ChartDimension + Legend ChartLegend + Title ChartTitle + VaryColors *bool + XAxis ChartAxis + YAxis ChartAxis + PlotArea ChartPlotArea + ShowBlanksAs string + HoleSize int + order int +} + +// ChartLegend directly maps the format settings of the chart legend. +type ChartLegend struct { + None bool + Position *string + ShowLegendKey bool +} + +// ChartMarker directly maps the format settings of the chart marker. +type ChartMarker struct { + Symbol string + Size int +} + +// ChartLine directly maps the format settings of the chart line. +type ChartLine struct { + Color string + Smooth bool + Width float64 +} + +// ChartSeries directly maps the format settings of the chart series. +type ChartSeries struct { + Name string + Categories string + Values string + Line ChartLine + Marker ChartMarker +} + +// ChartTitle directly maps the format settings of the chart title. +type ChartTitle struct { + Name string } diff --git a/xmlComments.go b/xmlComments.go index 13b727b780..c559cc9390 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -74,9 +74,9 @@ type xlsxPhoneticRun struct { // Comment directly maps the comment information. type Comment struct { - Author string `json:"author"` - AuthorID int `json:"author_id"` - Cell string `json:"cell"` - Text string `json:"text"` - Runs []RichTextRun `json:"runs"` + Author string + AuthorID int + Cell string + Text string + Runs []RichTextRun } diff --git a/xmlDrawing.go b/xmlDrawing.go index 9af6905e34..4df01b412a 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -121,7 +121,14 @@ const ( // PivotTables chosen are created in a version of Excel earlier than // Excel 2007 or in compatibility mode. Slicer can only be used with // PivotTables created in Excel 2007 or a newer version of Excel. - pivotTableVersion = 3 + pivotTableVersion = 3 + defaultPictureScale = 1.0 + defaultChartDimensionWidth = 480 + defaultChartDimensionHeight = 290 + defaultChartLegendPosition = "bottom" + defaultChartShowBlanksAs = "gap" + defaultShapeSize = 160 + defaultShapeLineWidth = 1 ) // ColorMappingType is the type of color transformation. @@ -554,48 +561,48 @@ type xdrTxBody struct { P []*aP `xml:"a:p"` } -// pictureOptions directly maps the format settings of the picture. -type pictureOptions struct { - FPrintsWithSheet bool `json:"print_obj"` - FLocksWithSheet bool `json:"locked"` - NoChangeAspect bool `json:"lock_aspect_ratio"` - Autofit bool `json:"autofit"` - OffsetX int `json:"x_offset"` - OffsetY int `json:"y_offset"` - XScale float64 `json:"x_scale"` - YScale float64 `json:"y_scale"` - Hyperlink string `json:"hyperlink"` - HyperlinkType string `json:"hyperlink_type"` - Positioning string `json:"positioning"` -} - -// shapeOptions directly maps the format settings of the shape. -type shapeOptions struct { - Macro string `json:"macro"` - Type string `json:"type"` - Width int `json:"width"` - Height int `json:"height"` - Format pictureOptions `json:"format"` - Color shapeColorOptions `json:"color"` - Line lineOptions `json:"line"` - Paragraph []shapeParagraphOptions `json:"paragraph"` -} - -// shapeParagraphOptions directly maps the format settings of the paragraph in +// PictureOptions directly maps the format settings of the picture. +type PictureOptions struct { + PrintObject *bool + Locked *bool + LockAspectRatio bool + AutoFit bool + OffsetX int + OffsetY int + XScale *float64 + YScale *float64 + Hyperlink string + HyperlinkType string + Positioning string +} + +// Shape directly maps the format settings of the shape. +type Shape struct { + Macro string + Type string + Width *int + Height *int + Format PictureOptions + Color ShapeColor + Line ShapeLine + Paragraph []ShapeParagraph +} + +// ShapeParagraph directly maps the format settings of the paragraph in // the shape. -type shapeParagraphOptions struct { - Font Font `json:"font"` - Text string `json:"text"` +type ShapeParagraph struct { + Font Font + Text string } -// shapeColorOptions directly maps the color settings of the shape. -type shapeColorOptions struct { - Line string `json:"line"` - Fill string `json:"fill"` - Effect string `json:"effect"` +// ShapeColor directly maps the color settings of the shape. +type ShapeColor struct { + Line string + Fill string + Effect string } -// lineOptions directly maps the line settings of the shape. -type lineOptions struct { - Width float64 `json:"width"` +// ShapeLine directly maps the line settings of the shape. +type ShapeLine struct { + Width *float64 } diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 7dac544236..3249ecacf7 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -83,6 +83,6 @@ type xlsxRPr struct { // RichTextRun directly maps the settings of the rich text run. type RichTextRun struct { - Font *Font `json:"font"` - Text string `json:"text"` + Font *Font + Text string } diff --git a/xmlStyles.go b/xmlStyles.go index c9e0761f73..2864c8b0d8 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -314,63 +314,63 @@ type xlsxStyleColors struct { // Alignment directly maps the alignment settings of the cells. type Alignment struct { - Horizontal string `json:"horizontal"` - Indent int `json:"indent"` - JustifyLastLine bool `json:"justify_last_line"` - ReadingOrder uint64 `json:"reading_order"` - RelativeIndent int `json:"relative_indent"` - ShrinkToFit bool `json:"shrink_to_fit"` - TextRotation int `json:"text_rotation"` - Vertical string `json:"vertical"` - WrapText bool `json:"wrap_text"` + Horizontal string + Indent int + JustifyLastLine bool + ReadingOrder uint64 + RelativeIndent int + ShrinkToFit bool + TextRotation int + Vertical string + WrapText bool } // Border directly maps the border settings of the cells. type Border struct { - Type string `json:"type"` - Color string `json:"color"` - Style int `json:"style"` + Type string + Color string + Style int } // Font directly maps the font settings of the fonts. type Font struct { - Bold bool `json:"bold"` - Italic bool `json:"italic"` - Underline string `json:"underline"` - Family string `json:"family"` - Size float64 `json:"size"` - Strike bool `json:"strike"` - Color string `json:"color"` - ColorIndexed int `json:"color_indexed"` - ColorTheme *int `json:"color_theme"` - ColorTint float64 `json:"color_tint"` - VertAlign string `json:"vertAlign"` + Bold bool + Italic bool + Underline string + Family string + Size float64 + Strike bool + Color string + ColorIndexed int + ColorTheme *int + ColorTint float64 + VertAlign string } // Fill directly maps the fill settings of the cells. type Fill struct { - Type string `json:"type"` - Pattern int `json:"pattern"` - Color []string `json:"color"` - Shading int `json:"shading"` + Type string + Pattern int + Color []string + Shading int } // Protection directly maps the protection settings of the cells. type Protection struct { - Hidden bool `json:"hidden"` - Locked bool `json:"locked"` + Hidden bool + Locked bool } // Style directly maps the style settings of the cells. type Style struct { - Border []Border `json:"border"` - Fill Fill `json:"fill"` - Font *Font `json:"font"` - Alignment *Alignment `json:"alignment"` - Protection *Protection `json:"protection"` - NumFmt int `json:"number_format"` - DecimalPlaces int `json:"decimal_places"` - CustomNumFmt *string `json:"custom_number_format"` - Lang string `json:"lang"` - NegRed bool `json:"negred"` + Border []Border + Fill Fill + Font *Font + Alignment *Alignment + Protection *Protection + NumFmt int + DecimalPlaces int + CustomNumFmt *string + Lang string + NegRed bool } diff --git a/xmlTable.go b/xmlTable.go index 758e0ea27c..3a5ded6e6b 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -196,22 +196,25 @@ type xlsxTableStyleInfo struct { ShowColumnStripes bool `xml:"showColumnStripes,attr"` } -// tableOptions directly maps the format settings of the table. -type tableOptions struct { - TableName string `json:"table_name"` - TableStyle string `json:"table_style"` - ShowFirstColumn bool `json:"show_first_column"` - ShowLastColumn bool `json:"show_last_column"` - ShowRowStripes bool `json:"show_row_stripes"` - ShowColumnStripes bool `json:"show_column_stripes"` -} - -// autoFilterOptions directly maps the auto filter settings. -type autoFilterOptions struct { - Column string `json:"column"` - Expression string `json:"expression"` - FilterList []struct { - Column string `json:"column"` - Value []int `json:"value"` - } `json:"filter_list"` +// TableOptions directly maps the format settings of the table. +type TableOptions struct { + Name string + StyleName string + ShowFirstColumn bool + ShowLastColumn bool + ShowRowStripes *bool + ShowColumnStripes bool +} + +// AutoFilterListOptions directly maps the auto filter list settings. +type AutoFilterListOptions struct { + Column string + Value []int +} + +// AutoFilterOptions directly maps the auto filter settings. +type AutoFilterOptions struct { + Column string + Expression string + FilterList []AutoFilterListOptions } diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 503eac160e..0d88596ed9 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -308,23 +308,23 @@ type xlsxCustomWorkbookView struct { // DefinedName directly maps the name for a cell or cell range on a // worksheet. type DefinedName struct { - Name string `json:"name,omitempty"` - Comment string `json:"comment,omitempty"` - RefersTo string `json:"refers_to,omitempty"` - Scope string `json:"scope,omitempty"` + Name string + Comment string + RefersTo string + Scope string } // WorkbookPropsOptions directly maps the settings of workbook proprieties. type WorkbookPropsOptions struct { - Date1904 *bool `json:"date_1994,omitempty"` - FilterPrivacy *bool `json:"filter_privacy,omitempty"` - CodeName *string `json:"code_name,omitempty"` + Date1904 *bool + FilterPrivacy *bool + CodeName *string } // WorkbookProtectionOptions directly maps the settings of workbook protection. type WorkbookProtectionOptions struct { - AlgorithmName string `json:"algorithmName,omitempty"` - Password string `json:"password,omitempty"` - LockStructure bool `json:"lockStructure,omitempty"` - LockWindows bool `json:"lockWindows,omitempty"` + AlgorithmName string + Password string + LockStructure bool + LockWindows bool } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 263c2a30ca..be7a5c92cc 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -794,141 +794,143 @@ type xlsxX14Sparkline struct { // SparklineOptions directly maps the settings of the sparkline. type SparklineOptions struct { - Location []string `json:"location"` - Range []string `json:"range"` - Max int `json:"max"` - CustMax int `json:"cust_max"` - Min int `json:"min"` - CustMin int `json:"cust_min"` - Type string `json:"hype"` - Weight float64 `json:"weight"` - DateAxis bool `json:"date_axis"` - Markers bool `json:"markers"` - High bool `json:"high"` - Low bool `json:"low"` - First bool `json:"first"` - Last bool `json:"last"` - Negative bool `json:"negative"` - Axis bool `json:"axis"` - Hidden bool `json:"hidden"` - Reverse bool `json:"reverse"` - Style int `json:"style"` - SeriesColor string `json:"series_color"` - NegativeColor string `json:"negative_color"` - MarkersColor string `json:"markers_color"` - FirstColor string `json:"first_color"` - LastColor string `json:"last_color"` - HightColor string `json:"hight_color"` - LowColor string `json:"low_color"` - EmptyCells string `json:"empty_cells"` -} - -// panesOptions directly maps the settings of the panes. -type panesOptions struct { - Freeze bool `json:"freeze"` - Split bool `json:"split"` - XSplit int `json:"x_split"` - YSplit int `json:"y_split"` - TopLeftCell string `json:"top_left_cell"` - ActivePane string `json:"active_pane"` - Panes []struct { - SQRef string `json:"sqref"` - ActiveCell string `json:"active_cell"` - Pane string `json:"pane"` - } `json:"panes"` -} - -// conditionalOptions directly maps the conditional format settings of the cells. -type conditionalOptions struct { - Type string `json:"type"` - AboveAverage bool `json:"above_average,omitempty"` - Percent bool `json:"percent,omitempty"` - Format int `json:"format,omitempty"` - Criteria string `json:"criteria,omitempty"` - Value string `json:"value,omitempty"` - Minimum string `json:"minimum,omitempty"` - Maximum string `json:"maximum,omitempty"` - MinType string `json:"min_type,omitempty"` - MidType string `json:"mid_type,omitempty"` - MaxType string `json:"max_type,omitempty"` - MinValue string `json:"min_value,omitempty"` - MidValue string `json:"mid_value,omitempty"` - MaxValue string `json:"max_value,omitempty"` - MinColor string `json:"min_color,omitempty"` - MidColor string `json:"mid_color,omitempty"` - MaxColor string `json:"max_color,omitempty"` - MinLength string `json:"min_length,omitempty"` - MaxLength string `json:"max_length,omitempty"` - MultiRange string `json:"multi_range,omitempty"` - BarColor string `json:"bar_color,omitempty"` + Location []string + Range []string + Max int + CustMax int + Min int + CustMin int + Type string + Weight float64 + DateAxis bool + Markers bool + High bool + Low bool + First bool + Last bool + Negative bool + Axis bool + Hidden bool + Reverse bool + Style int + SeriesColor string + NegativeColor string + MarkersColor string + FirstColor string + LastColor string + HightColor string + LowColor string + EmptyCells string +} + +// PaneOptions directly maps the settings of the pane. +type PaneOptions struct { + SQRef string + ActiveCell string + Pane string +} + +// Panes directly maps the settings of the panes. +type Panes struct { + Freeze bool + Split bool + XSplit int + YSplit int + TopLeftCell string + ActivePane string + Panes []PaneOptions +} + +// ConditionalFormatOptions directly maps the conditional format settings of the cells. +type ConditionalFormatOptions struct { + Type string + AboveAverage bool + Percent bool + Format int + Criteria string + Value string + Minimum string + Maximum string + MinType string + MidType string + MaxType string + MinValue string + MidValue string + MaxValue string + MinColor string + MidColor string + MaxColor string + MinLength string + MaxLength string + BarColor string } // SheetProtectionOptions directly maps the settings of worksheet protection. type SheetProtectionOptions struct { - AlgorithmName string `json:"algorithm_name,omitempty"` - AutoFilter bool `json:"auto_filter,omitempty"` - DeleteColumns bool `json:"delete_columns,omitempty"` - DeleteRows bool `json:"delete_rows,omitempty"` - EditObjects bool `json:"edit_objects,omitempty"` - EditScenarios bool `json:"edit_scenarios,omitempty"` - FormatCells bool `json:"format_cells,omitempty"` - FormatColumns bool `json:"format_columns,omitempty"` - FormatRows bool `json:"format_rows,omitempty"` - InsertColumns bool `json:"insert_columns,omitempty"` - InsertHyperlinks bool `json:"insert_hyperlinks,omitempty"` - InsertRows bool `json:"insert_rows,omitempty"` - Password string `json:"password,omitempty"` - PivotTables bool `json:"pivot_tables,omitempty"` - SelectLockedCells bool `json:"select_locked_cells,omitempty"` - SelectUnlockedCells bool `json:"select_unlocked_cells,omitempty"` - Sort bool `json:"sort,omitempty"` + AlgorithmName string + AutoFilter bool + DeleteColumns bool + DeleteRows bool + EditObjects bool + EditScenarios bool + FormatCells bool + FormatColumns bool + FormatRows bool + InsertColumns bool + InsertHyperlinks bool + InsertRows bool + Password string + PivotTables bool + SelectLockedCells bool + SelectUnlockedCells bool + Sort bool } // HeaderFooterOptions directly maps the settings of header and footer. type HeaderFooterOptions struct { - AlignWithMargins bool `json:"align_with_margins,omitempty"` - DifferentFirst bool `json:"different_first,omitempty"` - DifferentOddEven bool `json:"different_odd_even,omitempty"` - ScaleWithDoc bool `json:"scale_with_doc,omitempty"` - OddHeader string `json:"odd_header,omitempty"` - OddFooter string `json:"odd_footer,omitempty"` - EvenHeader string `json:"even_header,omitempty"` - EvenFooter string `json:"even_footer,omitempty"` - FirstHeader string `json:"first_header,omitempty"` - FirstFooter string `json:"first_footer,omitempty"` + AlignWithMargins bool + DifferentFirst bool + DifferentOddEven bool + ScaleWithDoc bool + OddHeader string + OddFooter string + EvenHeader string + EvenFooter string + FirstHeader string + FirstFooter string } // PageLayoutMarginsOptions directly maps the settings of page layout margins. type PageLayoutMarginsOptions struct { - Bottom *float64 `json:"bottom,omitempty"` - Footer *float64 `json:"footer,omitempty"` - Header *float64 `json:"header,omitempty"` - Left *float64 `json:"left,omitempty"` - Right *float64 `json:"right,omitempty"` - Top *float64 `json:"top,omitempty"` - Horizontally *bool `json:"horizontally,omitempty"` - Vertically *bool `json:"vertically,omitempty"` + Bottom *float64 + Footer *float64 + Header *float64 + Left *float64 + Right *float64 + Top *float64 + Horizontally *bool + Vertically *bool } // PageLayoutOptions directly maps the settings of page layout. type PageLayoutOptions struct { // Size defines the paper size of the worksheet. - Size *int `json:"size,omitempty"` + Size *int // Orientation defines the orientation of page layout for a worksheet. - Orientation *string `json:"orientation,omitempty"` + Orientation *string // FirstPageNumber specified the first printed page number. If no value is // specified, then 'automatic' is assumed. - FirstPageNumber *uint `json:"first_page_number,omitempty"` + FirstPageNumber *uint // AdjustTo defines the print scaling. This attribute is restricted to // value ranging from 10 (10%) to 400 (400%). This setting is overridden // when fitToWidth and/or fitToHeight are in use. - AdjustTo *uint `json:"adjust_to,omitempty"` + AdjustTo *uint // FitToHeight specified the number of vertical pages to fit on. - FitToHeight *int `json:"fit_to_height,omitempty"` + FitToHeight *int // FitToWidth specified the number of horizontal pages to fit on. - FitToWidth *int `json:"fit_to_width,omitempty"` + FitToWidth *int // BlackAndWhite specified print black and white. - BlackAndWhite *bool `json:"black_and_white,omitempty"` + BlackAndWhite *bool } // ViewOptions directly maps the settings of sheet view. @@ -936,37 +938,37 @@ type ViewOptions struct { // DefaultGridColor indicating that the consuming application should use // the default grid lines color(system dependent). Overrides any color // specified in colorId. - DefaultGridColor *bool `json:"default_grid_color,omitempty"` + DefaultGridColor *bool // RightToLeft indicating whether the sheet is in 'right to left' display // mode. When in this mode, Column A is on the far right, Column B; is one // column left of Column A, and so on. Also, information in cells is // displayed in the Right to Left format. - RightToLeft *bool `json:"right_to_left,omitempty"` + RightToLeft *bool // ShowFormulas indicating whether this sheet should display formulas. - ShowFormulas *bool `json:"show_formulas,omitempty"` + ShowFormulas *bool // ShowGridLines indicating whether this sheet should display grid lines. - ShowGridLines *bool `json:"show_grid_lines,omitempty"` + ShowGridLines *bool // ShowRowColHeaders indicating whether the sheet should display row and // column headings. - ShowRowColHeaders *bool `json:"show_row_col_headers,omitempty"` + ShowRowColHeaders *bool // ShowRuler indicating this sheet should display ruler. - ShowRuler *bool `json:"show_ruler,omitempty"` + ShowRuler *bool // ShowZeros indicating whether to "show a zero in cells that have zero // value". When using a formula to reference another cell which is empty, // the referenced value becomes 0 when the flag is true. (Default setting // is true.) - ShowZeros *bool `json:"show_zeros,omitempty"` + ShowZeros *bool // TopLeftCell specifies a location of the top left visible cell Location // of the top left visible cell in the bottom right pane (when in // Left-to-Right mode). - TopLeftCell *string `json:"top_left_cell,omitempty"` + TopLeftCell *string // View indicating how sheet is displayed, by default it uses empty string // available options: normal, pageLayout, pageBreakPreview - View *string `json:"low_color,omitempty"` + View *string // ZoomScale specifies a window zoom magnification for current view // representing percent values. This attribute is restricted to values // ranging from 10 to 400. Horizontal & Vertical scale together. - ZoomScale *float64 `json:"zoom_scale,omitempty"` + ZoomScale *float64 } // SheetPropsOptions directly maps the settings of sheet view. @@ -974,55 +976,55 @@ type SheetPropsOptions struct { // Specifies a stable name of the sheet, which should not change over time, // and does not change from user input. This name should be used by code // to reference a particular sheet. - CodeName *string `json:"code_name,omitempty"` + CodeName *string // EnableFormatConditionsCalculation indicating whether the conditional // formatting calculations shall be evaluated. If set to false, then the // min/max values of color scales or data bars or threshold values in Top N // rules shall not be updated. Essentially the conditional // formatting "calc" is off. - EnableFormatConditionsCalculation *bool `json:"enable_format_conditions_calculation,omitempty"` + EnableFormatConditionsCalculation *bool // Published indicating whether the worksheet is published. - Published *bool `json:"published,omitempty"` + Published *bool // AutoPageBreaks indicating whether the sheet displays Automatic Page // Breaks. - AutoPageBreaks *bool `json:"auto_page_breaks,omitempty"` + AutoPageBreaks *bool // FitToPage indicating whether the Fit to Page print option is enabled. - FitToPage *bool `json:"fit_to_page,omitempty"` + FitToPage *bool // TabColorIndexed represents the indexed color value. - TabColorIndexed *int `json:"tab_color_indexed,omitempty"` + TabColorIndexed *int // TabColorRGB represents the standard Alpha Red Green Blue color value. - TabColorRGB *string `json:"tab_color_rgb,omitempty"` + TabColorRGB *string // TabColorTheme represents the zero-based index into the collection, // referencing a particular value expressed in the Theme part. - TabColorTheme *int `json:"tab_color_theme,omitempty"` + TabColorTheme *int // TabColorTint specifies the tint value applied to the color. - TabColorTint *float64 `json:"tab_color_tint,omitempty"` + TabColorTint *float64 // OutlineSummaryBelow indicating whether summary rows appear below detail // in an outline, when applying an outline. - OutlineSummaryBelow *bool `json:"outline_summary_below,omitempty"` + OutlineSummaryBelow *bool // OutlineSummaryRight indicating whether summary columns appear to the // right of detail in an outline, when applying an outline. - OutlineSummaryRight *bool `json:"outline_summary_right,omitempty"` + OutlineSummaryRight *bool // BaseColWidth specifies the number of characters of the maximum digit // width of the normal style's font. This value does not include margin // padding or extra padding for grid lines. It is only the number of // characters. - BaseColWidth *uint8 `json:"base_col_width,omitempty"` + BaseColWidth *uint8 // DefaultColWidth specifies the default column width measured as the // number of characters of the maximum digit width of the normal style's // font. - DefaultColWidth *float64 `json:"default_col_width,omitempty"` + DefaultColWidth *float64 // DefaultRowHeight specifies the default row height measured in point // size. Optimization so we don't have to write the height on all rows. // This can be written out if most rows have custom height, to achieve the // optimization. - DefaultRowHeight *float64 `json:"default_row_height,omitempty"` + DefaultRowHeight *float64 // CustomHeight specifies the custom height. - CustomHeight *bool `json:"custom_height,omitempty"` + CustomHeight *bool // ZeroHeight specifies if rows are hidden. - ZeroHeight *bool `json:"zero_height,omitempty"` + ZeroHeight *bool // ThickTop specifies if rows have a thick top border by default. - ThickTop *bool `json:"thick_top,omitempty"` + ThickTop *bool // ThickBottom specifies if rows have a thick bottom border by default. - ThickBottom *bool `json:"thick_bottom,omitempty"` + ThickBottom *bool } From b39626fae9c2aaa648259d412a67b67e7768ad17 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 2 Jan 2023 11:47:31 +0800 Subject: [PATCH 142/213] This fixed worksheet protection issue - Update example code in the documentation - Update unit tests - Rename `PictureOptions` to `GraphicOptions` - Adjust partial options fields data types for the `PictureOptions` and `Shape` structure - Update dependencies module --- LICENSE | 2 +- README.md | 22 +++++- README_zh.md | 22 +++++- adjust.go | 4 +- calc.go | 4 +- calcchain.go | 4 +- cell.go | 42 +++++++---- cell_test.go | 13 +++- chart.go | 166 ++++++++++++++++++++++++++--------------- chart_test.go | 59 +++++++-------- col.go | 5 +- comment.go | 6 +- comment_test.go | 4 +- crypt.go | 4 +- crypt_test.go | 4 +- datavalidation.go | 4 +- datavalidation_test.go | 4 +- date.go | 4 +- docProps.go | 4 +- docProps_test.go | 4 +- drawing.go | 16 ++-- drawing_test.go | 4 +- errors.go | 4 +- excelize.go | 15 ++-- excelize_test.go | 12 ++- file.go | 4 +- go.mod | 6 +- go.sum | 17 +++-- lib.go | 4 +- merge.go | 4 +- numfmt.go | 4 +- picture.go | 72 +++++++++++------- picture_test.go | 19 +++-- pivotTable.go | 9 ++- rows.go | 8 +- shape.go | 32 ++++---- shape_test.go | 14 ++-- sheet.go | 60 +++++++-------- sheetpr.go | 4 +- sheetview.go | 4 +- sparkline.go | 4 +- stream.go | 13 +++- styles.go | 36 ++++++--- styles_test.go | 6 +- table.go | 21 +++--- templates.go | 4 +- vmlDrawing.go | 4 +- workbook.go | 4 +- xmlApp.go | 4 +- xmlCalcChain.go | 4 +- xmlChart.go | 13 ++-- xmlChartSheet.go | 4 +- xmlComments.go | 4 +- xmlContentTypes.go | 4 +- xmlCore.go | 4 +- xmlDecodeDrawing.go | 4 +- xmlDrawing.go | 18 ++--- xmlPivotCache.go | 4 +- xmlPivotTable.go | 4 +- xmlSharedStrings.go | 4 +- xmlStyles.go | 4 +- xmlTable.go | 4 +- xmlTheme.go | 4 +- xmlWorkbook.go | 4 +- xmlWorksheet.go | 4 +- 65 files changed, 498 insertions(+), 378 deletions(-) diff --git a/LICENSE b/LICENSE index 10897e7d4c..391f88aede 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2016-2022 The excelize Authors. +Copyright (c) 2016-2023 The excelize Authors. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index b261206a5b..48ff150807 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,17 @@ import ( func main() { f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() // Create a new sheet. - index := f.NewSheet("Sheet2") + index, err := f.NewSheet("Sheet2") + if err != nil { + fmt.Println(err) + return + } // Set value of a cell. f.SetCellValue("Sheet2", "A2", "Hello world.") f.SetCellValue("Sheet1", "B2", 100) @@ -122,6 +131,11 @@ import ( func main() { f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() for idx, row := range [][]interface{}{ {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3}, {"Normal", 5, 2, 4}, {"Large", 6, 7, 8}, @@ -196,14 +210,14 @@ func main() { fmt.Println(err) } // Insert a picture to worksheet with scaling. - enable, disable, scale := true, false, 0.5 if err := f.AddPicture("Sheet1", "D2", "image.jpg", - &excelize.PictureOptions{XScale: &scale, YScale: &scale}); err != nil { + &excelize.GraphicOptions{ScaleX: 0.5, ScaleY: 0.5}); err != nil { fmt.Println(err) } // Insert a picture offset in the cell with printing support. + enable, disable := true, false if err := f.AddPicture("Sheet1", "H2", "image.gif", - &excelize.PictureOptions{ + &excelize.GraphicOptions{ PrintObject: &enable, LockAspectRatio: false, OffsetX: 15, diff --git a/README_zh.md b/README_zh.md index ddd892e95e..b6c689bec5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -44,8 +44,17 @@ import ( func main() { f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() // 创建一个工作表 - index := f.NewSheet("Sheet2") + index, err := f.NewSheet("Sheet2") + if err != nil { + fmt.Println(err) + return + } // 设置单元格的值 f.SetCellValue("Sheet2", "A2", "Hello world.") f.SetCellValue("Sheet1", "B2", 100) @@ -122,6 +131,11 @@ import ( func main() { f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() for idx, row := range [][]interface{}{ {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3}, {"Normal", 5, 2, 4}, {"Large", 6, 7, 8}, @@ -196,14 +210,14 @@ func main() { fmt.Println(err) } // 在工作表中插入图片,并设置图片的缩放比例 - enable, disable, scale := true, false, 0.5 if err := f.AddPicture("Sheet1", "D2", "image.jpg", - &excelize.PictureOptions{XScale: &scale, YScale: &scale}); err != nil { + &excelize.GraphicOptions{ScaleX: 0.5, ScaleY: 0.5}); err != nil { fmt.Println(err) } // 在工作表中插入图片,并设置图片的打印属性 + enable, disable := true, false if err := f.AddPicture("Sheet1", "H2", "image.gif", - &excelize.PictureOptions{ + &excelize.GraphicOptions{ PrintObject: &enable, LockAspectRatio: false, OffsetX: 15, diff --git a/adjust.go b/adjust.go index de634fc114..95832c2be6 100644 --- a/adjust.go +++ b/adjust.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/calc.go b/calc.go index aeb00fe80a..895f78bc66 100644 --- a/calc.go +++ b/calc.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/calcchain.go b/calcchain.go index 5e511dc2eb..915508e7f4 100644 --- a/calcchain.go +++ b/calcchain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/cell.go b/cell.go index de08ffa88f..992a7412ac 100644 --- a/cell.go +++ b/cell.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -679,6 +679,11 @@ type FormulaOpts struct { // // func main() { // f := excelize.NewFile() +// defer func() { +// if err := f.Close(); err != nil { +// fmt.Println(err) +// } +// }() // for idx, row := range [][]interface{}{{"A", "B", "C"}, {1, 2}} { // if err := f.SetSheetRow("Sheet1", fmt.Sprintf("A%d", idx+1), &row); err != nil { // fmt.Println(err) @@ -1044,6 +1049,11 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { // // func main() { // f := excelize.NewFile() +// defer func() { +// if err := f.Close(); err != nil { +// fmt.Println(err) +// } +// }() // if err := f.SetRowHeight("Sheet1", 1, 35); err != nil { // fmt.Println(err) // return @@ -1395,39 +1405,39 @@ func (f *File) mergeCellsParser(ws *xlsxWorksheet, cell string) (string, error) // checkCellInRangeRef provides a function to determine if a given cell reference // in a range. -func (f *File) checkCellInRangeRef(cell, reference string) (bool, error) { +func (f *File) checkCellInRangeRef(cell, rangeRef string) (bool, error) { col, row, err := CellNameToCoordinates(cell) if err != nil { return false, err } - if rng := strings.Split(reference, ":"); len(rng) != 2 { + if rng := strings.Split(rangeRef, ":"); len(rng) != 2 { return false, err } - coordinates, err := rangeRefToCoordinates(reference) + coordinates, err := rangeRefToCoordinates(rangeRef) if err != nil { return false, err } - return cellInRef([]int{col, row}, coordinates), err + return cellInRange([]int{col, row}, coordinates), err } -// cellInRef provides a function to determine if a given range is within a +// cellInRange provides a function to determine if a given range is within a // range. -func cellInRef(cell, ref []int) bool { +func cellInRange(cell, ref []int) bool { return cell[0] >= ref[0] && cell[0] <= ref[2] && cell[1] >= ref[1] && cell[1] <= ref[3] } // isOverlap find if the given two rectangles overlap or not. func isOverlap(rect1, rect2 []int) bool { - return cellInRef([]int{rect1[0], rect1[1]}, rect2) || - cellInRef([]int{rect1[2], rect1[1]}, rect2) || - cellInRef([]int{rect1[0], rect1[3]}, rect2) || - cellInRef([]int{rect1[2], rect1[3]}, rect2) || - cellInRef([]int{rect2[0], rect2[1]}, rect1) || - cellInRef([]int{rect2[2], rect2[1]}, rect1) || - cellInRef([]int{rect2[0], rect2[3]}, rect1) || - cellInRef([]int{rect2[2], rect2[3]}, rect1) + return cellInRange([]int{rect1[0], rect1[1]}, rect2) || + cellInRange([]int{rect1[2], rect1[1]}, rect2) || + cellInRange([]int{rect1[0], rect1[3]}, rect2) || + cellInRange([]int{rect1[2], rect1[3]}, rect2) || + cellInRange([]int{rect2[0], rect2[1]}, rect1) || + cellInRange([]int{rect2[2], rect2[1]}, rect1) || + cellInRange([]int{rect2[0], rect2[3]}, rect1) || + cellInRange([]int{rect2[2], rect2[3]}, rect1) } // parseSharedFormula generate dynamic part of shared formula for target cell diff --git a/cell_test.go b/cell_test.go index e387793080..cee218891a 100644 --- a/cell_test.go +++ b/cell_test.go @@ -43,7 +43,7 @@ func TestConcurrency(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet1", "A3", "A3", style)) // Concurrency add picture assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), - &PictureOptions{ + &GraphicOptions{ OffsetX: 10, OffsetY: 10, Hyperlink: "https://github.com/xuri/excelize", @@ -475,11 +475,20 @@ func TestGetCellFormula(t *testing.T) { func ExampleFile_SetCellFloat() { f := NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() x := 3.14159265 if err := f.SetCellFloat("Sheet1", "A1", x, 2, 64); err != nil { fmt.Println(err) } - val, _ := f.GetCellValue("Sheet1", "A1") + val, err := f.GetCellValue("Sheet1", "A1") + if err != nil { + fmt.Println(err) + return + } fmt.Println(val) // Output: 3.14 } diff --git a/chart.go b/chart.go index a9d96a0172..b983388f2d 100644 --- a/chart.go +++ b/chart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -483,11 +483,11 @@ func parseChartOptions(opts *Chart) (*Chart, error) { if opts == nil { return nil, ErrParameterInvalid } - if opts.Dimension.Width == nil { - opts.Dimension.Width = intPtr(defaultChartDimensionWidth) + if opts.Dimension.Width == 0 { + opts.Dimension.Width = defaultChartDimensionWidth } - if opts.Dimension.Height == nil { - opts.Dimension.Height = intPtr(defaultChartDimensionHeight) + if opts.Dimension.Height == 0 { + opts.Dimension.Height = defaultChartDimensionHeight } if opts.Format.PrintObject == nil { opts.Format.PrintObject = boolPtr(true) @@ -495,14 +495,14 @@ func parseChartOptions(opts *Chart) (*Chart, error) { if opts.Format.Locked == nil { opts.Format.Locked = boolPtr(false) } - if opts.Format.XScale == nil { - opts.Format.XScale = float64Ptr(defaultPictureScale) + if opts.Format.ScaleX == 0 { + opts.Format.ScaleX = defaultPictureScale } - if opts.Format.YScale == nil { - opts.Format.YScale = float64Ptr(defaultPictureScale) + if opts.Format.ScaleY == 0 { + opts.Format.ScaleY = defaultPictureScale } - if opts.Legend.Position == nil { - opts.Legend.Position = stringPtr(defaultChartLegendPosition) + if opts.Legend.Position == "" { + opts.Legend.Position = defaultChartLegendPosition } if opts.Title.Name == "" { opts.Title.Name = " " @@ -531,6 +531,11 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // // func main() { // f := excelize.NewFile() +// defer func() { +// if err := f.Close(); err != nil { +// fmt.Println(err) +// } +// }() // for idx, row := range [][]interface{}{ // {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3}, // {"Normal", 5, 2, 4}, {"Large", 6, 7, 8}, @@ -542,7 +547,6 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // } // f.SetSheetRow("Sheet1", cell, &row) // } -// positionBottom := "bottom" // if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ // Type: "col3DClustered", // Series: []excelize.ChartSeries{ @@ -566,7 +570,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Name: "Fruit 3D Clustered Column Chart", // }, // Legend: excelize.ChartLegend{ -// None: false, Position: &positionBottom, ShowLegendKey: false, +// ShowLegendKey: false, // }, // PlotArea: excelize.ChartPlotArea{ // ShowBubbleSize: true, @@ -646,7 +650,8 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // bubble | bubble chart // bubble3D | 3D bubble chart // -// In Excel a chart series is a collection of information that defines which data is plotted such as values, axis labels and formatting. +// In Excel a chart series is a collection of information that defines which +// data is plotted such as values, axis labels and formatting. // // The series options that can be set are: // @@ -656,15 +661,29 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Line // Marker // -// Name: Set the name for the series. The name is displayed in the chart legend and in the formula bar. The 'Name' property is optional and if it isn't supplied it will default to Series 1..n. The name can also be a formula such as Sheet1!$A$1 +// Name: Set the name for the series. The name is displayed in the chart legend +// and in the formula bar. The 'Name' property is optional and if it isn't +// supplied it will default to Series 1..n. The name can also be a formula such +// as Sheet1!$A$1 // -// Categories: This sets the chart category labels. The category is more or less the same as the X axis. In most chart types the 'Categories' property is optional and the chart will just assume a sequential series from 1..n. +// Categories: This sets the chart category labels. The category is more or less +// the same as the X axis. In most chart types the 'Categories' property is +// optional and the chart will just assume a sequential series from 1..n. // -// Values: This is the most important property of a series and is the only mandatory option for every chart object. This option links the chart with the worksheet data that it displays. +// Values: This is the most important property of a series and is the only +// mandatory option for every chart object. This option links the chart with +// the worksheet data that it displays. // -// Line: This sets the line format of the line chart. The 'Line' property is optional and if it isn't supplied it will default style. The options that can be set are width and color. The range of width is 0.25pt - 999pt. If the value of width is outside the range, the default width of the line is 2pt. The value for color should be represented in hex format (e.g., #000000 - #FFFFFF) +// Line: This sets the line format of the line chart. The 'Line' property is +// optional and if it isn't supplied it will default style. The options that +// can be set are width and color. The range of width is 0.25pt - 999pt. If the +// value of width is outside the range, the default width of the line is 2pt. +// The value for color should be represented in hex format +// (e.g., #000000 - #FFFFFF) // -// Marker: This sets the marker of the line chart and scatter chart. The range of optional field 'size' is 2-72 (default value is 5). The enumeration value of optional field 'Symbol' are (default value is 'auto'): +// Marker: This sets the marker of the line chart and scatter chart. The range +// of optional field 'Size' is 2-72 (default value is 5). The enumeration value +// of optional field 'Symbol' are (default value is 'auto'): // // circle // dash @@ -681,29 +700,33 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // // Set properties of the chart legend. The options that can be set are: // -// None // Position // ShowLegendKey // -// None: Specified if show the legend without overlapping the chart. The default value is 'false'. -// -// Position: Set the position of the chart legend. The default legend position is right. This parameter only takes effect when 'none' is false. The available positions are: +// Position: Set the position of the chart legend. The default legend position +// is bottom. The available positions are: // +// none // top // bottom // left // right // top_right // -// ShowLegendKey: Set the legend keys shall be shown in data labels. The default value is false. +// ShowLegendKey: Set the legend keys shall be shown in data labels. The default +// value is false. // // Set properties of the chart title. The properties that can be set are: // // Title // -// Name: Set the name (title) for the chart. The name is displayed above the chart. The name can also be a formula such as Sheet1!$A$1 or a list with a sheet name. The name property is optional. The default is to have no chart title. +// Name: Set the name (title) for the chart. The name is displayed above the +// chart. The name can also be a formula such as Sheet1!$A$1 or a list with a +// sheet name. The name property is optional. The default is to have no chart +// title. // -// Specifies how blank cells are plotted on the chart by ShowBlanksAs. The default value is gap. The options that can be set are: +// Specifies how blank cells are plotted on the chart by 'ShowBlanksAs'. The +// default value is gap. The options that can be set are: // // gap // span @@ -715,11 +738,14 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // // zero: Specifies that blank values shall be treated as zero. // -// Specifies that each data marker in the series has a different color by VaryColors. The default value is true. +// Specifies that each data marker in the series has a different color by +// 'VaryColors'. The default value is true. // -// Set chart offset, scale, aspect ratio setting and print settings by format, same as function AddPicture. +// Set chart offset, scale, aspect ratio setting and print settings by format, +// same as function 'AddPicture'. // -// Set the position of the chart plot area by PlotArea. The properties that can be set are: +// Set the position of the chart plot area by PlotArea. The properties that can +// be set are: // // ShowBubbleSize // ShowCatName @@ -728,19 +754,26 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // ShowSerName // ShowVal // -// ShowBubbleSize: Specifies the bubble size shall be shown in a data label. The ShowBubbleSize property is optional. The default value is false. +// ShowBubbleSize: Specifies the bubble size shall be shown in a data label. The +// 'ShowBubbleSize' property is optional. The default value is false. // -// ShowCatName: Specifies that the category name shall be shown in the data label. The ShowCatName property is optional. The default value is true. +// ShowCatName: Specifies that the category name shall be shown in the data +// label. The 'ShowCatName' property is optional. The default value is true. // -// ShowLeaderLines: Specifies leader lines shall be shown for data labels. The ShowLeaderLines property is optional. The default value is false. +// ShowLeaderLines: Specifies leader lines shall be shown for data labels. The +// 'ShowLeaderLines' property is optional. The default value is false. // -// ShowPercent: Specifies that the percentage shall be shown in a data label. The ShowPercent property is optional. The default value is false. +// ShowPercent: Specifies that the percentage shall be shown in a data label. +// The 'ShowPercent' property is optional. The default value is false. // -// ShowSerName: Specifies that the series name shall be shown in a data label. The ShowSerName property is optional. The default value is false. +// ShowSerName: Specifies that the series name shall be shown in a data label. +// The 'ShowSerName' property is optional. The default value is false. // -// ShowVal: Specifies that the value shall be shown in a data label. The ShowVal property is optional. The default value is false. +// ShowVal: Specifies that the value shall be shown in a data label. +// The 'ShowVal' property is optional. The default value is false. // -// Set the primary horizontal and vertical axis options by XAxis and YAxis. The properties of XAxis that can be set are: +// Set the primary horizontal and vertical axis options by 'XAxis' and 'YAxis'. +// The properties of XAxis that can be set are: // // None // MajorGridLines @@ -751,7 +784,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Minimum // Font // -// The properties of YAxis that can be set are: +// The properties of 'YAxis' that can be set are: // // None // MajorGridLines @@ -769,17 +802,25 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // // MinorGridLines: Specifies minor grid lines. // -// MajorUnit: Specifies the distance between major ticks. Shall contain a positive floating-point number. The MajorUnit property is optional. The default value is auto. +// MajorUnit: Specifies the distance between major ticks. Shall contain a +// positive floating-point number. The MajorUnit property is optional. The +// default value is auto. // -// TickLabelSkip: Specifies how many tick labels to skip between label that is drawn. The TickLabelSkip property is optional. The default value is auto. +// TickLabelSkip: Specifies how many tick labels to skip between label that is +// drawn. The 'TickLabelSkip' property is optional. The default value is auto. // -// ReverseOrder: Specifies that the categories or values on reverse order (orientation of the chart). The ReverseOrder property is optional. The default value is false. +// ReverseOrder: Specifies that the categories or values on reverse order +// (orientation of the chart). The ReverseOrder property is optional. The +// default value is false. // -// Maximum: Specifies that the fixed maximum, 0 is auto. The Maximum property is optional. The default value is auto. +// Maximum: Specifies that the fixed maximum, 0 is auto. The 'Maximum' property +// is optional. The default value is auto. // -// Minimum: Specifies that the fixed minimum, 0 is auto. The Minimum property is optional. The default value is auto. +// Minimum: Specifies that the fixed minimum, 0 is auto. The 'Minimum' property +// is optional. The default value is auto. // -// Font: Specifies that the font of the horizontal and vertical axis. The properties of font that can be set are: +// Font: Specifies that the font of the horizontal and vertical axis. The +// properties of font that can be set are: // // Bold // Italic @@ -790,10 +831,11 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Color // VertAlign // -// Set chart size by dimension property. The dimension property is optional. The default width is 480, and height is 290. +// Set chart size by 'Dimension' property. The 'Dimension' property is optional. +// The default width is 480, and height is 290. // -// combo: Specifies the create a chart that combines two or more chart types -// in a single chart. For example, create a clustered column - line chart with +// combo: Specifies the create a chart that combines two or more chart types in +// a single chart. For example, create a clustered column - line chart with // data Sheet1!$E$1:$L$15: // // package main @@ -806,6 +848,11 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // // func main() { // f := excelize.NewFile() +// defer func() { +// if err := f.Close(); err != nil { +// fmt.Println(err) +// } +// }() // for idx, row := range [][]interface{}{ // {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3}, // {"Normal", 5, 2, 4}, {"Large", 6, 7, 8}, @@ -817,8 +864,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // } // f.SetSheetRow("Sheet1", cell, &row) // } -// enable, disable, scale := true, false, 1.0 -// positionLeft, positionRight := "left", "right" +// enable, disable := true, false // if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ // Type: "col", // Series: []excelize.ChartSeries{ @@ -828,9 +874,9 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Values: "Sheet1!$B$2:$D$2", // }, // }, -// Format: excelize.Picture{ -// XScale: &scale, -// YScale: &scale, +// Format: excelize.GraphicOptions{ +// ScaleX: 1, +// ScaleY: 1, // OffsetX: 15, // OffsetY: 10, // PrintObject: &enable, @@ -841,10 +887,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Name: "Clustered Column - Line Chart", // }, // Legend: excelize.ChartLegend{ -// Position: &positionLeft, ShowLegendKey: false, +// Position: "left", +// ShowLegendKey: false, // }, // PlotArea: excelize.ChartPlotArea{ -// ShowBubbleSize: true, // ShowCatName: false, // ShowLeaderLines: false, // ShowPercent: true, @@ -863,9 +909,9 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // }, // }, // }, -// Format: excelize.Picture{ -// XScale: &scale, -// YScale: &scale, +// Format: excelize.GraphicOptions{ +// ScaleX: 1, +// ScaleY: 1, // OffsetX: 15, // OffsetY: 10, // PrintObject: &enable, @@ -873,10 +919,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Locked: &disable, // }, // Legend: excelize.ChartLegend{ -// Position: &positionRight, ShowLegendKey: false, +// Position: "right", +// ShowLegendKey: false, // }, // PlotArea: excelize.ChartPlotArea{ -// ShowBubbleSize: true, // ShowCatName: false, // ShowLeaderLines: false, // ShowPercent: true, @@ -909,7 +955,7 @@ func (f *File) AddChart(sheet, cell string, chart *Chart, combo ...*Chart) error drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML) drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") - err = f.addDrawingChart(sheet, drawingXML, cell, *opts.Dimension.Width, *opts.Dimension.Height, drawingRID, &opts.Format) + err = f.addDrawingChart(sheet, drawingXML, cell, int(opts.Dimension.Width), int(opts.Dimension.Height), drawingRID, &opts.Format) if err != nil { return err } diff --git a/chart_test.go b/chart_test.go index deed7dd98e..2c740ef324 100644 --- a/chart_test.go +++ b/chart_test.go @@ -41,12 +41,11 @@ func TestChartSize(t *testing.T) { assert.NoError(t, f.SetCellValue(sheet1, cell, v)) } - width, height := 640, 480 assert.NoError(t, f.AddChart("Sheet1", "E4", &Chart{ Type: "col3DClustered", Dimension: ChartDimension{ - Width: &width, - Height: &height, + Width: 640, + Height: 480, }, Series: []ChartSeries{ {Name: "Sheet1!$A$2", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$2:$D$2"}, @@ -107,14 +106,14 @@ func TestAddDrawingChart(t *testing.T) { path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) - assert.EqualError(t, f.addDrawingChart("Sheet1", path, "A1", 0, 0, 0, &PictureOptions{PrintObject: boolPtr(true), Locked: boolPtr(false), XScale: float64Ptr(defaultPictureScale), YScale: float64Ptr(defaultPictureScale)}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addDrawingChart("Sheet1", path, "A1", 0, 0, 0, &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)}), "XML syntax error on line 1: invalid UTF-8") } func TestAddSheetDrawingChart(t *testing.T) { f := NewFile() path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) - assert.EqualError(t, f.addSheetDrawingChart(path, 0, &PictureOptions{PrintObject: boolPtr(true), Locked: boolPtr(false), XScale: float64Ptr(defaultPictureScale), YScale: float64Ptr(defaultPictureScale)}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addSheetDrawingChart(path, 0, &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)}), "XML syntax error on line 1: invalid UTF-8") } func TestDeleteDrawing(t *testing.T) { @@ -142,7 +141,6 @@ func TestAddChart(t *testing.T) { // Test add chart on not exists worksheet assert.EqualError(t, f.AddChart("SheetN", "P1", nil), "sheet SheetN does not exist") - positionLeft, positionBottom, positionRight, positionTop, positionTopRight := "left", "bottom", "right", "top", "top_right" maximum, minimum, zero := 7.5, 0.5, .0 series := []ChartSeries{ {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}, @@ -165,16 +163,16 @@ func TestAddChart(t *testing.T) { {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Line: ChartLine{Width: 0.25}}, } series3 := []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}} - format := PictureOptions{ - XScale: float64Ptr(defaultPictureScale), - YScale: float64Ptr(defaultPictureScale), + format := GraphicOptions{ + ScaleX: defaultPictureScale, + ScaleY: defaultPictureScale, OffsetX: 15, OffsetY: 10, PrintObject: boolPtr(true), LockAspectRatio: false, Locked: boolPtr(false), } - legend := ChartLegend{Position: &positionLeft, ShowLegendKey: false} + legend := ChartLegend{Position: "left", ShowLegendKey: false} plotArea := ChartPlotArea{ ShowBubbleSize: true, ShowCatName: true, @@ -187,13 +185,13 @@ func TestAddChart(t *testing.T) { sheetName, cell string opts *Chart }{ - {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: "col", Series: series, Format: format, Legend: ChartLegend{None: true, ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "#000000"}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "#777777"}}}}, + {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: "col", Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "#000000"}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "#777777"}}}}, {sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: "colStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: "colPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: "col3DClustered", Series: series, Format: format, Legend: ChartLegend{Position: &positionBottom, ShowLegendKey: false}, Title: ChartTitle{Name: "3D Clustered Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: "col3DClustered", Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Clustered Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: "col3DStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: "col3DPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "X45", opts: &Chart{Type: "radar", Series: series, Format: format, Legend: ChartLegend{Position: &positionTopRight, ShowLegendKey: false}, Title: ChartTitle{Name: "Radar Chart"}, PlotArea: plotArea, ShowBlanksAs: "span"}}, + {sheetName: "Sheet1", cell: "X45", opts: &Chart{Type: "radar", Series: series, Format: format, Legend: ChartLegend{Position: "top_right", ShowLegendKey: false}, Title: ChartTitle{Name: "Radar Chart"}, PlotArea: plotArea, ShowBlanksAs: "span"}}, {sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: "col3DConeStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AF16", opts: &Chart{Type: "col3DConeClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AF30", opts: &Chart{Type: "col3DConePercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, @@ -207,12 +205,12 @@ func TestAddChart(t *testing.T) { {sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: "col3DCylinderPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AV45", opts: &Chart{Type: "col3DCylinder", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: "col3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: "line3D", Series: series2, Format: format, Legend: ChartLegend{Position: &positionTop, ShowLegendKey: false}, Title: ChartTitle{Name: "3D Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}}, - {sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: "scatter", Series: series, Format: format, Legend: ChartLegend{Position: &positionBottom, ShowLegendKey: false}, Title: ChartTitle{Name: "Scatter Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: "doughnut", Series: series3, Format: format, Legend: ChartLegend{Position: &positionRight, ShowLegendKey: false}, Title: ChartTitle{Name: "Doughnut Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}}, - {sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: "line", Series: series2, Format: format, Legend: ChartLegend{Position: &positionTop, ShowLegendKey: false}, Title: ChartTitle{Name: "Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}}, - {sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: "pie3D", Series: series3, Format: format, Legend: ChartLegend{Position: &positionBottom, ShowLegendKey: false}, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: "pie", Series: series3, Format: format, Legend: ChartLegend{Position: &positionBottom, ShowLegendKey: false}, Title: ChartTitle{Name: "Pie Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "gap"}}, + {sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: "line3D", Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}}, + {sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: "scatter", Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Scatter Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: "doughnut", Series: series3, Format: format, Legend: ChartLegend{Position: "right", ShowLegendKey: false}, Title: ChartTitle{Name: "Doughnut Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}}, + {sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: "line", Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}}, + {sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: "pie3D", Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: "pie", Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Pie Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "gap"}}, // bar series chart {sheetName: "Sheet2", cell: "P48", opts: &Chart{Type: "bar", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "X48", opts: &Chart{Type: "barStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, @@ -343,7 +341,6 @@ func TestDeleteChart(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) assert.NoError(t, f.DeleteChart("Sheet1", "A1")) - positionLeft := "left" series := []ChartSeries{ {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}, {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31"}, @@ -354,16 +351,16 @@ func TestDeleteChart(t *testing.T) { {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36"}, {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"}, } - format := PictureOptions{ - XScale: float64Ptr(defaultPictureScale), - YScale: float64Ptr(defaultPictureScale), + format := GraphicOptions{ + ScaleX: defaultPictureScale, + ScaleY: defaultPictureScale, OffsetX: 15, OffsetY: 10, PrintObject: boolPtr(true), LockAspectRatio: false, Locked: boolPtr(false), } - legend := ChartLegend{Position: &positionLeft, ShowLegendKey: false} + legend := ChartLegend{Position: "left", ShowLegendKey: false} plotArea := ChartPlotArea{ ShowBubbleSize: true, ShowCatName: true, @@ -416,17 +413,17 @@ func TestChartWithLogarithmicBase(t *testing.T) { assert.NoError(t, f.SetCellValue(sheet1, cell, v)) } series := []ChartSeries{{Name: "value", Categories: "Sheet1!$A$1:$A$19", Values: "Sheet1!$B$1:$B$10"}} - dimension := []int{640, 480, 320, 240} + dimension := []uint{640, 480, 320, 240} for _, c := range []struct { cell string opts *Chart }{ - {cell: "C1", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[0], Height: &dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart without log scaling"}}}, - {cell: "M1", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[0], Height: &dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart with log 10.5 scaling"}, YAxis: ChartAxis{LogBase: 10.5}}}, - {cell: "A25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[2], Height: &dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1.9 scaling"}, YAxis: ChartAxis{LogBase: 1.9}}}, - {cell: "F25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[2], Height: &dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 2 scaling"}, YAxis: ChartAxis{LogBase: 2}}}, - {cell: "K25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[2], Height: &dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000.1 scaling"}, YAxis: ChartAxis{LogBase: 1000.1}}}, - {cell: "P25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: &dimension[2], Height: &dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000 scaling"}, YAxis: ChartAxis{LogBase: 1000}}}, + {cell: "C1", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart without log scaling"}}}, + {cell: "M1", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart with log 10.5 scaling"}, YAxis: ChartAxis{LogBase: 10.5}}}, + {cell: "A25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1.9 scaling"}, YAxis: ChartAxis{LogBase: 1.9}}}, + {cell: "F25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 2 scaling"}, YAxis: ChartAxis{LogBase: 2}}}, + {cell: "K25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000.1 scaling"}, YAxis: ChartAxis{LogBase: 1000.1}}}, + {cell: "P25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000 scaling"}, YAxis: ChartAxis{LogBase: 1000}}}, } { // Add two chart, one without and one with log scaling assert.NoError(t, f.AddChart(sheet1, c.cell, c.opts)) diff --git a/col.go b/col.go index f8906109b6..bb1ffd5ce4 100644 --- a/col.go +++ b/col.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -475,7 +475,6 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { // SetColWidth provides a function to set the width of a single column or // multiple columns. This function is concurrency safe. For example: // -// f := excelize.NewFile() // err := f.SetColWidth("Sheet1", "A", "H", 20) func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error { min, max, err := f.parseColRange(startCol + ":" + endCol) diff --git a/comment.go b/comment.go index 395d7c1acd..8206e685e6 100644 --- a/comment.go +++ b/comment.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -87,7 +87,7 @@ func (f *File) getSheetComments(sheetFile string) string { // author length is 255 and the max text length is 32512. For example, add a // comment in Sheet1!$A$30: // -// err := f.AddComment(sheet, excelize.Comment{ +// err := f.AddComment("Sheet1", excelize.Comment{ // Cell: "A12", // Author: "Excelize", // Runs: []excelize.RichTextRun{ diff --git a/comment_test.go b/comment_test.go index 0f668f1bff..7acce4820d 100644 --- a/comment_test.go +++ b/comment_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/crypt.go b/crypt.go index dc8e35f652..13985336c8 100644 --- a/crypt.go +++ b/crypt.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/crypt_test.go b/crypt_test.go index a4a510e2a8..dfbaaf3514 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/datavalidation.go b/datavalidation.go index 5ae5f65932..1201b4fa8b 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/datavalidation_test.go b/datavalidation_test.go index 7260b1a74e..66855f74b2 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/date.go b/date.go index 3e81319dd7..b3cbb75c85 100644 --- a/date.go +++ b/date.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/docProps.go b/docProps.go index 0531d4c9e9..3dca7bd2ac 100644 --- a/docProps.go +++ b/docProps.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/docProps_test.go b/docProps_test.go index da16a0456f..dfe5536a9e 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/drawing.go b/drawing.go index de5a1d1c48..88aaab83fc 100644 --- a/drawing.go +++ b/drawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -139,7 +139,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { }, PlotArea: &cPlotArea{}, Legend: &cLegend{ - LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[*opts.Legend.Position])}, + LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[opts.Legend.Position])}, Overlay: &attrValBool{Val: boolPtr(false)}, }, @@ -237,7 +237,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { Bubble: f.drawBaseChart, Bubble3D: f.drawBaseChart, } - if opts.Legend.None { + if opts.Legend.Position == "none" { xlsxChartSpace.Chart.Legend = nil } addChart := func(c, p *cPlotArea) { @@ -1242,7 +1242,7 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int, error) { // addDrawingChart provides a function to add chart graphic frame by given // sheet, drawingXML, cell, width, height, relationship index and format sets. -func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, opts *PictureOptions) error { +func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, opts *GraphicOptions) error { col, row, err := CellNameToCoordinates(cell) if err != nil { return err @@ -1250,8 +1250,8 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI colIdx := col - 1 rowIdx := row - 1 - width = int(float64(width) * *opts.XScale) - height = int(float64(height) * *opts.YScale) + width = int(float64(width) * opts.ScaleX) + height = int(float64(height) * opts.ScaleY) colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.OffsetX, opts.OffsetY, width, height) content, cNvPrID, err := f.drawingParser(drawingXML) if err != nil { @@ -1304,7 +1304,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI // addSheetDrawingChart provides a function to add chart graphic frame for // chartsheet by given sheet, drawingXML, width, height, relationship index // and format sets. -func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *PictureOptions) error { +func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *GraphicOptions) error { content, cNvPrID, err := f.drawingParser(drawingXML) if err != nil { return err diff --git a/drawing_test.go b/drawing_test.go index f0580dda1f..7fcee82970 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/errors.go b/errors.go index d6e0b4198e..471afa62f5 100644 --- a/errors.go +++ b/errors.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/excelize.go b/excelize.go index f84afd6f29..3acdb43c0f 100644 --- a/excelize.go +++ b/excelize.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. // // See https://xuri.me/excelize for more information about this package. package excelize @@ -37,15 +37,15 @@ type File struct { sheetMap map[string]string streams map[string]*StreamWriter tempFiles sync.Map + sharedStringsMap map[string]int + sharedStringItem [][]uint + sharedStringTemp *os.File CalcChain *xlsxCalcChain Comments map[string]*xlsxComments ContentTypes *xlsxTypes Drawings sync.Map Path string SharedStrings *xlsxSST - sharedStringsMap map[string]int - sharedStringItem [][]uint - sharedStringTemp *os.File Sheet sync.Map SheetCount int Styles *xlsxStyleSheet @@ -58,6 +58,8 @@ type File struct { CharsetReader charsetTranscoderFn } +// charsetTranscoderFn set user-defined codepage transcoder function for open +// the spreadsheet from non-UTF-8 encoding. type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) // Options define the options for open and reading spreadsheet. @@ -92,9 +94,6 @@ type Options struct { // password protection: // // f, err := excelize.OpenFile("Book1.xlsx", excelize.Options{Password: "password"}) -// if err != nil { -// return -// } // // Close the file by Close function after opening the spreadsheet. func OpenFile(filename string, opts ...Options) (*File, error) { diff --git a/excelize_test.go b/excelize_test.go index b9e1403d7c..47d83a855f 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -353,9 +353,8 @@ func TestNewFile(t *testing.T) { f.SetActiveSheet(0) // Test add picture to sheet with scaling and positioning - scale := 0.5 assert.NoError(t, f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), - &PictureOptions{XScale: &scale, YScale: &scale, Positioning: "absolute"})) + &GraphicOptions{ScaleX: 0.5, ScaleY: 0.5, Positioning: "absolute"})) // Test add picture to worksheet without options assert.NoError(t, f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), nil)) @@ -1283,7 +1282,7 @@ func TestHSL(t *testing.T) { func TestProtectSheet(t *testing.T) { f := NewFile() sheetName := f.GetSheetName(0) - assert.NoError(t, f.ProtectSheet(sheetName, nil)) + assert.EqualError(t, f.ProtectSheet(sheetName, nil), ErrParameterInvalid.Error()) // Test protect worksheet with XOR hash algorithm assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{ Password: "password", @@ -1517,13 +1516,13 @@ func prepareTestBook1() (*File, error) { } if err = f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), - &PictureOptions{OffsetX: 140, OffsetY: 120, Hyperlink: "#Sheet2!D8", HyperlinkType: "Location"}); err != nil { + &GraphicOptions{OffsetX: 140, OffsetY: 120, Hyperlink: "#Sheet2!D8", HyperlinkType: "Location"}); err != nil { return nil, err } // Test add picture to worksheet with offset, external hyperlink and positioning if err := f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.png"), - &PictureOptions{ + &GraphicOptions{ OffsetX: 10, OffsetY: 10, Hyperlink: "https://github.com/xuri/excelize", @@ -1562,9 +1561,8 @@ func prepareTestBook3() (*File, error) { return nil, err } f.SetActiveSheet(0) - scale := 0.5 if err := f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), - &PictureOptions{XScale: &scale, YScale: &scale, Positioning: "absolute"}); err != nil { + &GraphicOptions{ScaleX: 0.5, ScaleY: 0.5, Positioning: "absolute"}); err != nil { return nil, err } if err := f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), nil); err != nil { diff --git a/file.go b/file.go index 30ae506f0e..51cd320d33 100644 --- a/file.go +++ b/file.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/go.mod b/go.mod index e3ed4bb9f5..aaa5a82421 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/stretchr/testify v1.8.0 github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 - golang.org/x/crypto v0.2.0 + golang.org/x/crypto v0.4.0 golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 - golang.org/x/net v0.2.0 - golang.org/x/text v0.4.0 + golang.org/x/net v0.4.0 + golang.org/x/text v0.5.0 ) require github.com/richardlehane/msoleps v1.0.3 // indirect diff --git a/go.sum b/go.sum index 6b97c1f2f1..65f6bfd63a 100644 --- a/go.sum +++ b/go.sum @@ -22,16 +22,17 @@ github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= -golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -39,15 +40,15 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/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/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/lib.go b/lib.go index d62b789b2b..e5637ec9f1 100644 --- a/lib.go +++ b/lib.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/merge.go b/merge.go index a839b96df1..b3138aff6b 100644 --- a/merge.go +++ b/merge.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/numfmt.go b/numfmt.go index 09e64c96d3..9b92140987 100644 --- a/numfmt.go +++ b/numfmt.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/picture.go b/picture.go index 72d3f7d983..067f1bf79e 100644 --- a/picture.go +++ b/picture.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -23,28 +23,28 @@ import ( "strings" ) -// parsePictureOptions provides a function to parse the format settings of +// parseGraphicOptions provides a function to parse the format settings of // the picture with default value. -func parsePictureOptions(opts *PictureOptions) *PictureOptions { +func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { if opts == nil { - return &PictureOptions{ + return &GraphicOptions{ PrintObject: boolPtr(true), - Locked: boolPtr(false), - XScale: float64Ptr(defaultPictureScale), - YScale: float64Ptr(defaultPictureScale), + Locked: boolPtr(true), + ScaleX: defaultPictureScale, + ScaleY: defaultPictureScale, } } if opts.PrintObject == nil { opts.PrintObject = boolPtr(true) } if opts.Locked == nil { - opts.Locked = boolPtr(false) + opts.Locked = boolPtr(true) } - if opts.XScale == nil { - opts.XScale = float64Ptr(defaultPictureScale) + if opts.ScaleX == 0 { + opts.ScaleX = defaultPictureScale } - if opts.YScale == nil { - opts.YScale = float64Ptr(defaultPictureScale) + if opts.ScaleY == 0 { + opts.ScaleY = defaultPictureScale } return opts } @@ -67,25 +67,32 @@ func parsePictureOptions(opts *PictureOptions) *PictureOptions { // // func main() { // f := excelize.NewFile() +// defer func() { +// if err := f.Close(); err != nil { +// fmt.Println(err) +// } +// }() // // Insert a picture. // if err := f.AddPicture("Sheet1", "A2", "image.jpg", nil); err != nil { // fmt.Println(err) +// return // } // // Insert a picture scaling in the cell with location hyperlink. -// enable, scale := true, 0.5 +// enable := true // if err := f.AddPicture("Sheet1", "D2", "image.png", -// &excelize.PictureOptions{ -// XScale: &scale, -// YScale: &scale, +// &excelize.GraphicOptions{ +// ScaleX: 0.5, +// ScaleY: 0.5, // Hyperlink: "#Sheet2!D8", // HyperlinkType: "Location", // }, // ); err != nil { // fmt.Println(err) +// return // } // // Insert a picture offset in the cell with external hyperlink, printing and positioning support. // if err := f.AddPicture("Sheet1", "H2", "image.gif", -// &excelize.PictureOptions{ +// &excelize.GraphicOptions{ // PrintObject: &enable, // LockAspectRatio: false, // OffsetX: 15, @@ -96,6 +103,7 @@ func parsePictureOptions(opts *PictureOptions) *PictureOptions { // }, // ); err != nil { // fmt.Println(err) +// return // } // if err := f.SaveAs("Book1.xlsx"); err != nil { // fmt.Println(err) @@ -129,15 +137,15 @@ func parsePictureOptions(opts *PictureOptions) *PictureOptions { // The optional parameter "OffsetX" specifies the horizontal offset of the // image with the cell, the default value of that is 0. // -// The optional parameter "XScale" specifies the horizontal scale of images, +// The optional parameter "ScaleX" specifies the horizontal scale of images, // the default value of that is 1.0 which presents 100%. // // The optional parameter "OffsetY" specifies the vertical offset of the // image with the cell, the default value of that is 0. // -// The optional parameter "YScale" specifies the vertical scale of images, +// The optional parameter "ScaleY" specifies the vertical scale of images, // the default value of that is 1.0 which presents 100%. -func (f *File) AddPicture(sheet, cell, picture string, opts *PictureOptions) error { +func (f *File) AddPicture(sheet, cell, picture string, opts *GraphicOptions) error { var err error // Check picture exists first. if _, err = os.Stat(picture); os.IsNotExist(err) { @@ -170,26 +178,32 @@ func (f *File) AddPicture(sheet, cell, picture string, opts *PictureOptions) err // // func main() { // f := excelize.NewFile() -// +// defer func() { +// if err := f.Close(); err != nil { +// fmt.Println(err) +// } +// }() // file, err := os.ReadFile("image.jpg") // if err != nil { // fmt.Println(err) +// return // } // if err := f.AddPictureFromBytes("Sheet1", "A2", "Excel Logo", ".jpg", file, nil); err != nil { // fmt.Println(err) +// return // } // if err := f.SaveAs("Book1.xlsx"); err != nil { // fmt.Println(err) // } // } -func (f *File) AddPictureFromBytes(sheet, cell, name, extension string, file []byte, opts *PictureOptions) error { +func (f *File) AddPictureFromBytes(sheet, cell, name, extension string, file []byte, opts *GraphicOptions) error { var drawingHyperlinkRID int var hyperlinkType string ext, ok := supportedImageTypes[extension] if !ok { return ErrImgExt } - options := parsePictureOptions(opts) + options := parseGraphicOptions(opts) img, _, err := image.DecodeConfig(bytes.NewReader(file)) if err != nil { return err @@ -305,7 +319,7 @@ func (f *File) countDrawings() int { // addDrawingPicture provides a function to add picture by given sheet, // drawingXML, cell, file name, width, height relationship index and format // sets. -func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, hyperlinkRID int, img image.Config, opts *PictureOptions) error { +func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, hyperlinkRID int, img image.Config, opts *GraphicOptions) error { col, row, err := CellNameToCoordinates(cell) if err != nil { return err @@ -317,8 +331,8 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, return err } } else { - width = int(float64(width) * *opts.XScale) - height = int(float64(height) * *opts.YScale) + width = int(float64(width) * opts.ScaleX) + height = int(float64(height) * opts.ScaleY) } col-- row-- @@ -711,7 +725,7 @@ func (f *File) drawingsWriter() { } // drawingResize calculate the height and width after resizing. -func (f *File) drawingResize(sheet, cell string, width, height float64, opts *PictureOptions) (w, h, c, r int, err error) { +func (f *File) drawingResize(sheet, cell string, width, height float64, opts *GraphicOptions) (w, h, c, r int, err error) { var mergeCells []MergeCell mergeCells, err = f.GetMergeCells(sheet) if err != nil { @@ -754,6 +768,6 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Pi height, width = float64(cellHeight), width*asp } width, height = width-float64(opts.OffsetX), height-float64(opts.OffsetY) - w, h = int(width**opts.XScale), int(height**opts.YScale) + w, h = int(width*opts.ScaleX), int(height*opts.ScaleY) return } diff --git a/picture_test.go b/picture_test.go index 11243b7655..eda36ffc13 100644 --- a/picture_test.go +++ b/picture_test.go @@ -37,24 +37,24 @@ func TestAddPicture(t *testing.T) { // Test add picture to worksheet with offset and location hyperlink assert.NoError(t, f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), - &PictureOptions{OffsetX: 140, OffsetY: 120, Hyperlink: "#Sheet2!D8", HyperlinkType: "Location"})) + &GraphicOptions{OffsetX: 140, OffsetY: 120, Hyperlink: "#Sheet2!D8", HyperlinkType: "Location"})) // Test add picture to worksheet with offset, external hyperlink and positioning assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), - &PictureOptions{OffsetX: 10, OffsetY: 10, Hyperlink: "https://github.com/xuri/excelize", HyperlinkType: "External", Positioning: "oneCell"})) + &GraphicOptions{OffsetX: 10, OffsetY: 10, Hyperlink: "https://github.com/xuri/excelize", HyperlinkType: "External", Positioning: "oneCell"})) file, err := os.ReadFile(filepath.Join("test", "images", "excel.png")) assert.NoError(t, err) // Test add picture to worksheet with autofit - assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), &PictureOptions{AutoFit: true})) - assert.NoError(t, f.AddPicture("Sheet1", "B30", filepath.Join("test", "images", "excel.jpg"), &PictureOptions{OffsetX: 10, OffsetY: 10, AutoFit: true})) + assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true})) + assert.NoError(t, f.AddPicture("Sheet1", "B30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{OffsetX: 10, OffsetY: 10, AutoFit: true})) _, err = f.NewSheet("AddPicture") assert.NoError(t, err) assert.NoError(t, f.SetRowHeight("AddPicture", 10, 30)) assert.NoError(t, f.MergeCell("AddPicture", "B3", "D9")) assert.NoError(t, f.MergeCell("AddPicture", "B1", "D1")) - assert.NoError(t, f.AddPicture("AddPicture", "C6", filepath.Join("test", "images", "excel.jpg"), &PictureOptions{AutoFit: true})) - assert.NoError(t, f.AddPicture("AddPicture", "A1", filepath.Join("test", "images", "excel.jpg"), &PictureOptions{AutoFit: true})) + assert.NoError(t, f.AddPicture("AddPicture", "C6", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true})) + assert.NoError(t, f.AddPicture("AddPicture", "A1", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true})) // Test add picture to worksheet from bytes assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "Excel Logo", ".png", file, nil)) @@ -105,8 +105,7 @@ func TestAddPictureErrors(t *testing.T) { assert.NoError(t, f.AddPicture("Sheet1", "Q7", filepath.Join("test", "images", "excel.wmf"), nil)) assert.NoError(t, f.AddPicture("Sheet1", "Q13", filepath.Join("test", "images", "excel.emz"), nil)) assert.NoError(t, f.AddPicture("Sheet1", "Q19", filepath.Join("test", "images", "excel.wmz"), nil)) - xScale := 2.1 - assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", &PictureOptions{XScale: &xScale})) + assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", &GraphicOptions{ScaleX: 2.1})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx"))) assert.NoError(t, f.Close()) } @@ -198,7 +197,7 @@ func TestGetPicture(t *testing.T) { func TestAddDrawingPicture(t *testing.T) { // Test addDrawingPicture with illegal cell reference f := NewFile() - opts := &PictureOptions{PrintObject: boolPtr(true), Locked: boolPtr(false), XScale: float64Ptr(defaultPictureScale), YScale: float64Ptr(defaultPictureScale)} + opts := &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)} assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", "", 0, 0, image.Config{}, opts), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) path := "xl/drawings/drawing1.xml" @@ -254,7 +253,7 @@ func TestDrawingResize(t *testing.T) { ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} - assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), &PictureOptions{AutoFit: true}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestSetContentTypePartImageExtensions(t *testing.T) { diff --git a/pivotTable.go b/pivotTable.go index 381938edc4..4c8dee282c 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -96,6 +96,11 @@ type PivotTableField struct { // // func main() { // f := excelize.NewFile() +// defer func() { +// if err := f.Close(); err != nil { +// fmt.Println(err) +// } +// }() // // Create some data in a sheet // month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} // year := []int{2017, 2018, 2019} diff --git a/rows.go b/rows.go index 8e0bb63f85..4470d2e12e 100644 --- a/rows.go +++ b/rows.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -795,11 +795,11 @@ func (r *xlsxRow) hasAttr() bool { // // For example set style of row 1 on Sheet1: // -// err = f.SetRowStyle("Sheet1", 1, 1, styleID) +// err := f.SetRowStyle("Sheet1", 1, 1, styleID) // // Set style of rows 1 to 10 on Sheet1: // -// err = f.SetRowStyle("Sheet1", 1, 10, styleID) +// err := f.SetRowStyle("Sheet1", 1, 10, styleID) func (f *File) SetRowStyle(sheet string, start, end, styleID int) error { if end < start { start, end = end, start diff --git a/shape.go b/shape.go index 3e2db80f2a..d194e55751 100644 --- a/shape.go +++ b/shape.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -22,11 +22,11 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { if opts == nil { return nil, ErrParameterInvalid } - if opts.Width == nil { - opts.Width = intPtr(defaultShapeSize) + if opts.Width == 0 { + opts.Width = defaultShapeSize } - if opts.Height == nil { - opts.Height = intPtr(defaultShapeSize) + if opts.Height == 0 { + opts.Height = defaultShapeSize } if opts.Format.PrintObject == nil { opts.Format.PrintObject = boolPtr(true) @@ -34,11 +34,11 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { if opts.Format.Locked == nil { opts.Format.Locked = boolPtr(false) } - if opts.Format.XScale == nil { - opts.Format.XScale = float64Ptr(defaultPictureScale) + if opts.Format.ScaleX == 0 { + opts.Format.ScaleX = defaultPictureScale } - if opts.Format.YScale == nil { - opts.Format.YScale = float64Ptr(defaultPictureScale) + if opts.Format.ScaleY == 0 { + opts.Format.ScaleY = defaultPictureScale } if opts.Line.Width == nil { opts.Line.Width = float64Ptr(defaultShapeLineWidth) @@ -51,7 +51,7 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { // print settings) and properties set. For example, add text box (rect shape) // in Sheet1: // -// width, height, lineWidth := 180, 90, 1.2 +// lineWidth := 1.2 // err := f.AddShape("Sheet1", "G6", // &excelize.Shape{ // Type: "rect", @@ -63,14 +63,14 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { // Bold: true, // Italic: true, // Family: "Times New Roman", -// Size: 36, +// Size: 18, // Color: "#777777", // Underline: "sng", // }, // }, // }, -// Width: &width, -// Height: &height, +// Width: 180, +// Height: 40, // Line: excelize.ShapeLine{Width: &lineWidth}, // }, // ) @@ -329,8 +329,8 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro colIdx := fromCol - 1 rowIdx := fromRow - 1 - width := int(float64(*opts.Width) * *opts.Format.XScale) - height := int(float64(*opts.Height) * *opts.Format.YScale) + width := int(float64(opts.Width) * opts.Format.ScaleX) + height := int(float64(opts.Height) * opts.Format.ScaleY) colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.Format.OffsetX, opts.Format.OffsetY, width, height) diff --git a/shape_test.go b/shape_test.go index bddc8d22de..436140865e 100644 --- a/shape_test.go +++ b/shape_test.go @@ -46,7 +46,7 @@ func TestAddShape(t *testing.T) { // Test add first shape for given sheet f = NewFile() - width, height := 1.2, 90 + lineWidth := 1.2 assert.NoError(t, f.AddShape("Sheet1", "A1", &Shape{ Type: "ellipseRibbon", @@ -63,8 +63,8 @@ func TestAddShape(t *testing.T) { }, }, }, - Height: &height, - Line: ShapeLine{Width: &width}, + Height: 90, + Line: ShapeLine{Width: &lineWidth}, })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) // Test add shape with invalid sheet name @@ -86,13 +86,11 @@ func TestAddDrawingShape(t *testing.T) { f.Pkg.Store(path, MacintoshCyrillicCharset) assert.EqualError(t, f.addDrawingShape("sheet1", path, "A1", &Shape{ - Width: intPtr(defaultShapeSize), - Height: intPtr(defaultShapeSize), - Format: PictureOptions{ + Width: defaultShapeSize, + Height: defaultShapeSize, + Format: GraphicOptions{ PrintObject: boolPtr(true), Locked: boolPtr(false), - XScale: float64Ptr(defaultPictureScale), - YScale: float64Ptr(defaultPictureScale), }, }, ), "XML syntax error on line 1: invalid UTF-8") diff --git a/sheet.go b/sheet.go index cbafdd2800..15e19c5b1f 100644 --- a/sheet.go +++ b/sheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -642,9 +642,12 @@ func (f *File) deleteSheetFromContentTypes(target string) error { // workbooks that contain tables, charts or pictures. For Example: // // // Sheet1 already exists... -// index := f.NewSheet("Sheet2") +// index, err := f.NewSheet("Sheet2") +// if err != nil { +// fmt.Println(err) +// return +// } // err := f.CopySheet(1, index) -// return err func (f *File) CopySheet(from, to int) error { if from < 0 || to < 0 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" { return ErrSheetIdx @@ -878,7 +881,6 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error { // TopLeftCell: "N57", // ActivePane: "bottomLeft", // Panes: []excelize.PaneOptions{ -// {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"}, // {SQRef: "I36", ActiveCell: "I36"}, // {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"}, // {SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"}, @@ -1213,9 +1215,11 @@ func (f *File) SetHeaderFooter(sheet string, settings *HeaderFooterOptions) erro // Sheet1 with protection settings: // // err := f.ProtectSheet("Sheet1", &excelize.SheetProtectionOptions{ -// AlgorithmName: "SHA-512", -// Password: "password", -// EditScenarios: false, +// AlgorithmName: "SHA-512", +// Password: "password", +// SelectLockedCells: true, +// SelectUnlockedCells: true, +// EditScenarios: true, // }) func (f *File) ProtectSheet(sheet string, opts *SheetProtectionOptions) error { ws, err := f.workSheetReader(sheet) @@ -1223,29 +1227,25 @@ func (f *File) ProtectSheet(sheet string, opts *SheetProtectionOptions) error { return err } if opts == nil { - opts = &SheetProtectionOptions{ - EditObjects: true, - EditScenarios: true, - SelectLockedCells: true, - } + return ErrParameterInvalid } ws.SheetProtection = &xlsxSheetProtection{ - AutoFilter: opts.AutoFilter, - DeleteColumns: opts.DeleteColumns, - DeleteRows: opts.DeleteRows, - FormatCells: opts.FormatCells, - FormatColumns: opts.FormatColumns, - FormatRows: opts.FormatRows, - InsertColumns: opts.InsertColumns, - InsertHyperlinks: opts.InsertHyperlinks, - InsertRows: opts.InsertRows, - Objects: opts.EditObjects, - PivotTables: opts.PivotTables, - Scenarios: opts.EditScenarios, - SelectLockedCells: opts.SelectLockedCells, - SelectUnlockedCells: opts.SelectUnlockedCells, + AutoFilter: !opts.AutoFilter, + DeleteColumns: !opts.DeleteColumns, + DeleteRows: !opts.DeleteRows, + FormatCells: !opts.FormatCells, + FormatColumns: !opts.FormatColumns, + FormatRows: !opts.FormatRows, + InsertColumns: !opts.InsertColumns, + InsertHyperlinks: !opts.InsertHyperlinks, + InsertRows: !opts.InsertRows, + Objects: !opts.EditObjects, + PivotTables: !opts.PivotTables, + Scenarios: !opts.EditScenarios, + SelectLockedCells: !opts.SelectLockedCells, + SelectUnlockedCells: !opts.SelectUnlockedCells, Sheet: true, - Sort: opts.Sort, + Sort: !opts.Sort, } if opts.Password != "" { if opts.AlgorithmName == "" { @@ -1532,7 +1532,7 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) { // or worksheet. If not specified scope, the default scope is workbook. // For example: // -// f.SetDefinedName(&excelize.DefinedName{ +// err := f.SetDefinedName(&excelize.DefinedName{ // Name: "Amount", // RefersTo: "Sheet1!$A$2:$D$5", // Comment: "defined name comment", @@ -1579,7 +1579,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { // workbook or worksheet. If not specified scope, the default scope is // workbook. For example: // -// f.DeleteDefinedName(&excelize.DefinedName{ +// err := f.DeleteDefinedName(&excelize.DefinedName{ // Name: "Amount", // Scope: "Sheet2", // }) diff --git a/sheetpr.go b/sheetpr.go index 41ca08291b..41e7e98986 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/sheetview.go b/sheetview.go index 9845942d3b..65b1354c78 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/sparkline.go b/sparkline.go index 0c32462644..43a827e151 100644 --- a/sparkline.go +++ b/sparkline.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/stream.go b/stream.go index 7a17484aff..4be4defbf6 100644 --- a/stream.go +++ b/stream.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -56,10 +56,12 @@ type StreamWriter struct { // streamWriter, err := file.NewStreamWriter("Sheet1") // if err != nil { // fmt.Println(err) +// return // } // styleID, err := file.NewStyle(&excelize.Style{Font: &excelize.Font{Color: "#777777"}}) // if err != nil { // fmt.Println(err) +// return // } // if err := streamWriter.SetRow("A1", // []interface{}{ @@ -71,6 +73,7 @@ type StreamWriter struct { // }, // excelize.RowOpts{Height: 45, Hidden: false}); err != nil { // fmt.Println(err) +// return // } // for rowID := 2; rowID <= 102400; rowID++ { // row := make([]interface{}, 50) @@ -80,10 +83,12 @@ type StreamWriter struct { // cell, _ := excelize.CoordinatesToCellName(1, rowID) // if err := streamWriter.SetRow(cell, row); err != nil { // fmt.Println(err) +// return // } // } // if err := streamWriter.Flush(); err != nil { // fmt.Println(err) +// return // } // if err := file.SaveAs("Book1.xlsx"); err != nil { // fmt.Println(err) @@ -155,9 +160,9 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { // called after the rows are written but before Flush. // // See File.AddTable for details on the table format. -func (sw *StreamWriter) AddTable(reference string, opts *TableOptions) error { +func (sw *StreamWriter) AddTable(rangeRef string, opts *TableOptions) error { options := parseTableOptions(opts) - coordinates, err := rangeRefToCoordinates(reference) + coordinates, err := rangeRefToCoordinates(rangeRef) if err != nil { return err } diff --git a/styles.go b/styles.go index 6eb86e104b..e5d933cd6b 100644 --- a/styles.go +++ b/styles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -1966,9 +1966,21 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // as date type in Uruguay (Spanish) format for Sheet1!A6: // // f := excelize.NewFile() -// f.SetCellValue("Sheet1", "A6", 42920.5) +// defer func() { +// if err := f.Close(); err != nil { +// fmt.Println(err) +// } +// }() +// if err := f.SetCellValue("Sheet1", "A6", 42920.5); err != nil { +// fmt.Println(err) +// return +// } // exp := "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@" // style, err := f.NewStyle(&excelize.Style{CustomNumFmt: &exp}) +// if err != nil { +// fmt.Println(err) +// return +// } // err = f.SetCellStyle("Sheet1", "A6", "A6", style) // // Cell Sheet1!A6 in the Excel Application: martes, 04 de Julio de 2017 @@ -2978,8 +2990,8 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // }, // ) // -// type: minimum - The minimum parameter is used to set the lower limiting value -// when the criteria is either "between" or "not between". +// type: Minimum - The 'Minimum' parameter is used to set the lower limiting +// value when the criteria is either "between" or "not between". // // // Highlight cells rules: between... // err := f.SetConditionalFormat("Sheet1", "A1:A10", @@ -2994,9 +3006,9 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // }, // ) // -// type: maximum - The maximum parameter is used to set the upper limiting value -// when the criteria is either "between" or "not between". See the previous -// example. +// type: Maximum - The 'Maximum' parameter is used to set the upper limiting +// value when the criteria is either "between" or "not between". See the +// previous example. // // type: average - The average type is used to specify Excel's "Average" style // conditional format: @@ -3178,7 +3190,7 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // MaxColor - Same as MinColor, see above. // // BarColor - Used for data_bar. Same as MinColor, see above. -func (f *File) SetConditionalFormat(sheet, reference string, opts []ConditionalFormatOptions) error { +func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFormatOptions) error { drawContFmtFunc := map[string]func(p int, ct string, fmtCond *ConditionalFormatOptions) *xlsxCfRule{ "cellIs": drawCondFmtCellIs, "top10": drawCondFmtTop10, @@ -3214,7 +3226,7 @@ func (f *File) SetConditionalFormat(sheet, reference string, opts []ConditionalF } ws.ConditionalFormatting = append(ws.ConditionalFormatting, &xlsxConditionalFormatting{ - SQRef: reference, + SQRef: rangeRef, CfRule: cfRule, }) return err @@ -3367,13 +3379,13 @@ func (f *File) GetConditionalFormats(sheet string) (map[string][]ConditionalForm // UnsetConditionalFormat provides a function to unset the conditional format // by given worksheet name and range reference. -func (f *File) UnsetConditionalFormat(sheet, reference string) error { +func (f *File) UnsetConditionalFormat(sheet, rangeRef string) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } for i, cf := range ws.ConditionalFormatting { - if cf.SQRef == reference { + if cf.SQRef == rangeRef { ws.ConditionalFormatting = append(ws.ConditionalFormatting[:i], ws.ConditionalFormatting[i+1:]...) return nil } diff --git a/styles_test.go b/styles_test.go index 44ba535d6d..53cd7cdd78 100644 --- a/styles_test.go +++ b/styles_test.go @@ -158,9 +158,9 @@ func TestSetConditionalFormat(t *testing.T) { for _, testCase := range cases { f := NewFile() const sheet = "Sheet1" - const cellRange = "A1:A1" + const rangeRef = "A1:A1" - err := f.SetConditionalFormat(sheet, cellRange, testCase.format) + err := f.SetConditionalFormat(sheet, rangeRef, testCase.format) if err != nil { t.Fatalf("%s", err) } @@ -170,7 +170,7 @@ func TestSetConditionalFormat(t *testing.T) { cf := ws.ConditionalFormatting assert.Len(t, cf, 1, testCase.label) assert.Len(t, cf[0].CfRule, 1, testCase.label) - assert.Equal(t, cellRange, cf[0].SQRef, testCase.label) + assert.Equal(t, rangeRef, cf[0].SQRef, testCase.label) assert.EqualValues(t, testCase.rules, cf[0].CfRule, testCase.label) } } diff --git a/table.go b/table.go index 42aa35a69f..d98fd7e61a 100644 --- a/table.go +++ b/table.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -39,6 +39,7 @@ func parseTableOptions(opts *TableOptions) *TableOptions { // // Create a table of F2:H6 on Sheet2 with format set: // +// disable := false // err := f.AddTable("Sheet2", "F2:H6", &excelize.TableOptions{ // Name: "table", // StyleName: "TableStyleMedium2", @@ -60,10 +61,10 @@ func parseTableOptions(opts *TableOptions) *TableOptions { // TableStyleLight1 - TableStyleLight21 // TableStyleMedium1 - TableStyleMedium28 // TableStyleDark1 - TableStyleDark11 -func (f *File) AddTable(sheet, reference string, opts *TableOptions) error { +func (f *File) AddTable(sheet, rangeRef string, opts *TableOptions) error { options := parseTableOptions(opts) // Coordinate conversion, convert C1:B3 to 2,0,1,2. - coordinates, err := rangeRefToCoordinates(reference) + coordinates, err := rangeRefToCoordinates(rangeRef) if err != nil { return err } @@ -261,8 +262,8 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab // x < 2000 // col < 2000 // Price < 2000 -func (f *File) AutoFilter(sheet, reference string, opts *AutoFilterOptions) error { - coordinates, err := rangeRefToCoordinates(reference) +func (f *File) AutoFilter(sheet, rangeRef string, opts *AutoFilterOptions) error { + coordinates, err := rangeRefToCoordinates(rangeRef) if err != nil { return err } @@ -302,13 +303,13 @@ func (f *File) AutoFilter(sheet, reference string, opts *AutoFilterOptions) erro wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) } } - refRange := coordinates[2] - coordinates[0] - return f.autoFilter(sheet, ref, refRange, coordinates[0], opts) + columns := coordinates[2] - coordinates[0] + return f.autoFilter(sheet, ref, columns, coordinates[0], opts) } // autoFilter provides a function to extract the tokens from the filter // expression. The tokens are mainly non-whitespace groups. -func (f *File) autoFilter(sheet, ref string, refRange, col int, opts *AutoFilterOptions) error { +func (f *File) autoFilter(sheet, ref string, columns, col int, opts *AutoFilterOptions) error { ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -330,7 +331,7 @@ func (f *File) autoFilter(sheet, ref string, refRange, col int, opts *AutoFilter return err } offset := fsCol - col - if offset < 0 || offset > refRange { + if offset < 0 || offset > columns { return fmt.Errorf("incorrect index of column '%s'", opts.Column) } diff --git a/templates.go b/templates.go index 2e0c051903..91a7ed80aa 100644 --- a/templates.go +++ b/templates.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. // // This file contains default templates for XML files we don't yet populated // based on content. diff --git a/vmlDrawing.go b/vmlDrawing.go index f9de49918e..be1212e649 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/workbook.go b/workbook.go index b3ee7ffafa..da4e2b1168 100644 --- a/workbook.go +++ b/workbook.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlApp.go b/xmlApp.go index abfd82b3d7..6109ec204c 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 9e25d50795..3631565aad 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlChart.go b/xmlChart.go index 10e6c2e3cd..9818ca1824 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -534,8 +534,8 @@ type ChartAxis struct { // ChartDimension directly maps the dimension of the chart. type ChartDimension struct { - Width *int - Height *int + Width uint + Height uint } // ChartPlotArea directly maps the format settings of the plot area. @@ -552,7 +552,7 @@ type ChartPlotArea struct { type Chart struct { Type string Series []ChartSeries - Format PictureOptions + Format GraphicOptions Dimension ChartDimension Legend ChartLegend Title ChartTitle @@ -567,8 +567,7 @@ type Chart struct { // ChartLegend directly maps the format settings of the chart legend. type ChartLegend struct { - None bool - Position *string + Position string ShowLegendKey bool } diff --git a/xmlChartSheet.go b/xmlChartSheet.go index f0f2f6286f..16599fd25c 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -9,7 +9,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlComments.go b/xmlComments.go index c559cc9390..214c15e743 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 52dd744c0f..950c09b68b 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlCore.go b/xmlCore.go index 18491319be..d28a71f63d 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index fb920be1d8..612bb62ed4 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlDrawing.go b/xmlDrawing.go index 4df01b412a..c3c524b59e 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize @@ -561,16 +561,16 @@ type xdrTxBody struct { P []*aP `xml:"a:p"` } -// PictureOptions directly maps the format settings of the picture. -type PictureOptions struct { +// GraphicOptions directly maps the format settings of the picture. +type GraphicOptions struct { PrintObject *bool Locked *bool LockAspectRatio bool AutoFit bool OffsetX int OffsetY int - XScale *float64 - YScale *float64 + ScaleX float64 + ScaleY float64 Hyperlink string HyperlinkType string Positioning string @@ -580,9 +580,9 @@ type PictureOptions struct { type Shape struct { Macro string Type string - Width *int - Height *int - Format PictureOptions + Width uint + Height uint + Format GraphicOptions Color ShapeColor Line ShapeLine Paragraph []ShapeParagraph diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 0af7c44d69..1925fa4d23 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 897669babc..163a801d6e 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 3249ecacf7..704002c7fb 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlStyles.go b/xmlStyles.go index 2864c8b0d8..070036cae5 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlTable.go b/xmlTable.go index 3a5ded6e6b..5710bc06d9 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlTheme.go b/xmlTheme.go index 80bb3afafe..3a01221fcc 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 0d88596ed9..f0d7b143d2 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize diff --git a/xmlWorksheet.go b/xmlWorksheet.go index be7a5c92cc..4e3a35860e 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -7,7 +7,7 @@ // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // Supports complex components by high compatibility, and provided streaming // API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.15 or later. +// data. This library needs Go version 1.16 or later. package excelize From 9c3a5eb9835e7b52350da3a2910b603c88f90bdc Mon Sep 17 00:00:00 2001 From: Liron Levin Date: Sat, 7 Jan 2023 07:17:00 +0200 Subject: [PATCH 143/213] Add missing error checks in `getSheetMap` to fix panic(#1437) Unit tests updated --- excelize.go | 4 +++- excelize_test.go | 5 ++++- sheet.go | 14 ++++++++++---- sheet_test.go | 6 ++++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/excelize.go b/excelize.go index 3acdb43c0f..b41cd00a23 100644 --- a/excelize.go +++ b/excelize.go @@ -179,7 +179,9 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) { if f.CalcChain, err = f.calcChainReader(); err != nil { return f, err } - f.sheetMap = f.getSheetMap() + if f.sheetMap, err = f.getSheetMap(); err != nil { + return f, err + } if f.Styles, err = f.stylesReader(); err != nil { return f, err } diff --git a/excelize_test.go b/excelize_test.go index 47d83a855f..a1857fe4dd 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -250,7 +250,10 @@ func TestOpenReader(t *testing.T) { assert.NoError(t, zw.Close()) return buf } - for _, defaultXMLPath := range []string{defaultXMLPathCalcChain, defaultXMLPathStyles} { + for _, defaultXMLPath := range []string{ + defaultXMLPathCalcChain, + defaultXMLPathStyles, + defaultXMLPathWorkbookRels} { _, err = OpenReader(preset(defaultXMLPath)) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } diff --git a/sheet.go b/sheet.go index 15e19c5b1f..a4be16a523 100644 --- a/sheet.go +++ b/sheet.go @@ -454,10 +454,16 @@ func (f *File) GetSheetList() (list []string) { // getSheetMap provides a function to get worksheet name and XML file path map // of the spreadsheet. -func (f *File) getSheetMap() map[string]string { +func (f *File) getSheetMap() (map[string]string, error) { maps := map[string]string{} - wb, _ := f.workbookReader() - rels, _ := f.relsReader(f.getWorkbookRelsPath()) + wb, err := f.workbookReader() + if err != nil { + return nil, err + } + rels, err := f.relsReader(f.getWorkbookRelsPath()) + if err != nil { + return nil, err + } for _, v := range wb.Sheets.Sheet { for _, rel := range rels.Relationships { if rel.ID == v.ID { @@ -471,7 +477,7 @@ func (f *File) getSheetMap() map[string]string { } } } - return maps + return maps, nil } // getSheetXMLPath provides a function to get XML file path by given sheet diff --git a/sheet_test.go b/sheet_test.go index 09b6155422..f809fe8f99 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -378,6 +378,12 @@ func TestGetSheetMap(t *testing.T) { } assert.Equal(t, len(sheetMap), 2) assert.NoError(t, f.Close()) + + f = NewFile() + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + _, err = f.getSheetMap() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestSetActiveSheet(t *testing.T) { From 5429f131f87a6c35564a44e491e1047af79510fb Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 8 Jan 2023 00:23:53 +0800 Subject: [PATCH 144/213] This closes #1438, fix cell data type issue for formula calculation engine - Update dependencies module - Update unit tests --- calc.go | 437 ++++++++++++++++++++++------------------------- calc_test.go | 172 ++++++++++++------- cell.go | 8 +- excelize_test.go | 3 +- go.mod | 6 +- go.sum | 17 +- 6 files changed, 335 insertions(+), 308 deletions(-) diff --git a/calc.go b/calc.go index 895f78bc66..b864a23e2f 100644 --- a/calc.go +++ b/calc.go @@ -768,28 +768,11 @@ type formulaFuncs struct { // Z.TEST // ZTEST func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { - return f.calcCellValue(&calcContext{ + var token formulaArg + token, err = f.calcCellValue(&calcContext{ entry: fmt.Sprintf("%s!%s", sheet, cell), iterations: make(map[string]uint), }, sheet, cell) -} - -func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result string, err error) { - var ( - formula string - token formulaArg - ) - if formula, err = f.GetCellFormula(sheet, cell); err != nil { - return - } - ps := efp.ExcelParser() - tokens := ps.Parse(formula) - if tokens == nil { - return - } - if token, err = f.evalInfixExp(ctx, sheet, cell, tokens); err != nil { - return - } result = token.Value() if isNum, precision, decimal := isNumeric(result); isNum { if precision > 15 { @@ -803,6 +786,22 @@ func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result strin return } +// calcCellValue calculate cell value by given context, worksheet name and cell +// reference. +func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result formulaArg, err error) { + var formula string + if formula, err = f.GetCellFormula(sheet, cell); err != nil { + return + } + ps := efp.ExcelParser() + tokens := ps.Parse(formula) + if tokens == nil { + return + } + result, err = f.evalInfixExp(ctx, sheet, cell, tokens) + return +} + // getPriority calculate arithmetic operator priority. func getPriority(token efp.Token) (pri int) { pri = tokenPriority[token.TValue] @@ -919,8 +918,8 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T if err != nil { return result, err } - if result.Type != ArgString { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE), errors.New(formulaErrorVALUE) + if result.Type == ArgError { + return result, errors.New(result.Error) } opfdStack.Push(result) continue @@ -933,7 +932,7 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T } result, err := f.parseReference(ctx, sheet, token.TValue) if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE), err + return newEmptyFormulaArg(), err } if result.Type == ArgUnknown { return newEmptyFormulaArg(), errors.New(formulaErrorVALUE) @@ -977,10 +976,6 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T continue } - // current token is logical - if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeLogical { - argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(token.TValue)) - } if inArrayRow && isOperand(token) { continue } @@ -1341,16 +1336,33 @@ func isOperatorPrefixToken(token efp.Token) bool { // isOperand determine if the token is parse operand. func isOperand(token efp.Token) bool { - return token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText) + return token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText || token.TSubType == efp.TokenSubTypeLogical) } // tokenToFormulaArg create a formula argument by given token. func tokenToFormulaArg(token efp.Token) formulaArg { - if token.TSubType == efp.TokenSubTypeNumber { + switch token.TSubType { + case efp.TokenSubTypeLogical: + return newBoolFormulaArg(strings.EqualFold(token.TValue, "TRUE")) + case efp.TokenSubTypeNumber: num, _ := strconv.ParseFloat(token.TValue, 64) return newNumberFormulaArg(num) + default: + return newStringFormulaArg(token.TValue) + } +} + +// formulaArgToToken create a token by given formula argument. +func formulaArgToToken(arg formulaArg) efp.Token { + switch arg.Type { + case ArgNumber: + if arg.Boolean { + return efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeLogical} + } + return efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber} + default: + return efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeText} } - return newStringFormulaArg(token.TValue) } // parseToken parse basic arithmetic operator priority and evaluate based on @@ -1366,12 +1378,7 @@ func (f *File) parseToken(ctx *calcContext, sheet string, token efp.Token, opdSt if err != nil { return errors.New(formulaErrorNAME) } - if result.Type != ArgString { - return errors.New(formulaErrorVALUE) - } - token.TValue = result.String - token.TType = efp.TokenTypeOperand - token.TSubType = efp.TokenSubTypeText + token = formulaArgToToken(result) } if isOperatorPrefixToken(token) { if err := f.parseOperatorPrefixToken(optStack, opdStack, token); err != nil { @@ -1505,20 +1512,39 @@ func prepareValueRef(cr cellRef, valueRange []int) { } // cellResolver calc cell value by given worksheet name, cell reference and context. -func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (string, error) { - var value string +func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, error) { + var ( + arg formulaArg + value string + err error + ) ref := fmt.Sprintf("%s!%s", sheet, cell) if formula, _ := f.GetCellFormula(sheet, cell); len(formula) != 0 { ctx.Lock() if ctx.entry != ref && ctx.iterations[ref] <= f.options.MaxCalcIterations { ctx.iterations[ref]++ ctx.Unlock() - value, _ = f.calcCellValue(ctx, sheet, cell) - return value, nil + arg, _ = f.calcCellValue(ctx, sheet, cell) + return arg, nil } ctx.Unlock() } - return f.GetCellValue(sheet, cell, Options{RawCellValue: true}) + if value, err = f.GetCellValue(sheet, cell, Options{RawCellValue: true}); err != nil { + return arg, err + } + arg = newStringFormulaArg(value) + cellType, _ := f.GetCellType(sheet, cell) + switch cellType { + case CellTypeBool: + return arg.ToBool(), err + case CellTypeNumber, CellTypeUnset: + if arg.Value() == "" { + return newEmptyFormulaArg(), err + } + return arg.ToNumber(), err + default: + return arg, err + } } // rangeResolver extract value as string from given reference and range list. @@ -1556,17 +1582,15 @@ func (f *File) rangeResolver(ctx *calcContext, cellRefs, cellRanges *list.List) for row := valueRange[0]; row <= valueRange[1]; row++ { var matrixRow []formulaArg for col := valueRange[2]; col <= valueRange[3]; col++ { - var cell, value string + var cell string + var value formulaArg if cell, err = CoordinatesToCellName(col, row); err != nil { return } if value, err = f.cellResolver(ctx, sheet, cell); err != nil { return } - matrixRow = append(matrixRow, formulaArg{ - String: value, - Type: ArgString, - }) + matrixRow = append(matrixRow, value) } arg.Matrix = append(arg.Matrix, matrixRow) } @@ -1579,10 +1603,10 @@ func (f *File) rangeResolver(ctx *calcContext, cellRefs, cellRanges *list.List) if cell, err = CoordinatesToCellName(cr.Col, cr.Row); err != nil { return } - if arg.String, err = f.cellResolver(ctx, cr.Sheet, cell); err != nil { + if arg, err = f.cellResolver(ctx, cr.Sheet, cell); err != nil { return } - arg.Type = ArgString + arg.cellRefs, arg.cellRanges = cellRefs, cellRanges } return } @@ -4618,10 +4642,11 @@ func newNumberMatrix(arg formulaArg, phalanx bool) (numMtx [][]float64, ele form } numMtx = append(numMtx, make([]float64, len(row))) for c, cell := range row { - if ele = cell.ToNumber(); ele.Type != ArgNumber { + if cell.Type != ArgNumber { + ele = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) return } - numMtx[r][c] = ele.Number + numMtx[r][c] = cell.Number } } return @@ -4946,31 +4971,24 @@ func (fn *formulaFuncs) POWER(argsList *list.List) formulaArg { // // PRODUCT(number1,[number2],...) func (fn *formulaFuncs) PRODUCT(argsList *list.List) formulaArg { - val, product := 0.0, 1.0 - var err error + product := 1.0 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { case ArgString: - if token.String == "" { - continue - } - if val, err = strconv.ParseFloat(token.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + num := token.ToNumber() + if num.Type != ArgNumber { + return num } - product = product * val + product = product * num.Number case ArgNumber: product = product * token.Number case ArgMatrix: for _, row := range token.Matrix { - for _, value := range row { - if value.Value() == "" { - continue - } - if val, err = strconv.ParseFloat(value.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + for _, cell := range row { + if cell.Type == ArgNumber { + product *= cell.Number } - product *= val } } } @@ -5685,26 +5703,23 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { if argsList.Len() == 3 { sumRange = argsList.Back().Value.(formulaArg).Matrix } - var sum, val float64 - var err error + var sum float64 + var arg formulaArg for rowIdx, row := range rangeMtx { - for colIdx, col := range row { - var ok bool - fromVal := col.String - if col.String == "" { + for colIdx, cell := range row { + arg = cell + if arg.Type == ArgEmpty { continue } - ok, _ = formulaCriteriaEval(fromVal, criteria) - if ok { + if ok, _ := formulaCriteriaEval(arg.Value(), criteria); ok { if argsList.Len() == 3 { if len(sumRange) > rowIdx && len(sumRange[rowIdx]) > colIdx { - fromVal = sumRange[rowIdx][colIdx].String + arg = sumRange[rowIdx][colIdx] } } - if val, err = strconv.ParseFloat(fromVal, 64); err != nil { - continue + if arg.Type == ArgNumber { + sum += arg.Number } - sum += val } } } @@ -7662,14 +7677,16 @@ func (fn *formulaFuncs) COUNT(argsList *list.List) formulaArg { for token := argsList.Front(); token != nil; token = token.Next() { arg := token.Value.(formulaArg) switch arg.Type { - case ArgString, ArgNumber: - if arg.ToNumber().Type != ArgError { + case ArgString: + if num := arg.ToNumber(); num.Type == ArgNumber { count++ } + case ArgNumber: + count++ case ArgMatrix: for _, row := range arg.Matrix { - for _, value := range row { - if value.ToNumber().Type != ArgError { + for _, cell := range row { + if cell.Type == ArgNumber { count++ } } @@ -7818,17 +7835,16 @@ func (fn *formulaFuncs) DEVSQ(argsList *list.List) formulaArg { } avg, count, result := fn.AVERAGE(argsList), -1, 0.0 for arg := argsList.Front(); arg != nil; arg = arg.Next() { - for _, number := range arg.Value.(formulaArg).ToList() { - num := number.ToNumber() - if num.Type != ArgNumber { + for _, cell := range arg.Value.(formulaArg).ToList() { + if cell.Type != ArgNumber { continue } count++ if count == 0 { - result = math.Pow(num.Number-avg.Number, 2) + result = math.Pow(cell.Number-avg.Number, 2) continue } - result += math.Pow(num.Number-avg.Number, 2) + result += math.Pow(cell.Number-avg.Number, 2) } } if count == -1 { @@ -9338,12 +9354,12 @@ func (fn *formulaFuncs) MODE(argsList *list.List) formulaArg { var values []float64 for arg := argsList.Front(); arg != nil; arg = arg.Next() { cells := arg.Value.(formulaArg) - if cells.Type != ArgMatrix && cells.ToNumber().Type != ArgNumber { + if cells.Type != ArgMatrix && cells.Type != ArgNumber { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } for _, cell := range cells.ToList() { - if num := cell.ToNumber(); num.Type == ArgNumber { - values = append(values, num.Number) + if cell.Type == ArgNumber { + values = append(values, cell.Number) } } } @@ -9381,12 +9397,12 @@ func (fn *formulaFuncs) MODEdotMULT(argsList *list.List) formulaArg { var values []float64 for arg := argsList.Front(); arg != nil; arg = arg.Next() { cells := arg.Value.(formulaArg) - if cells.Type != ArgMatrix && cells.ToNumber().Type != ArgNumber { + if cells.Type != ArgMatrix && cells.Type != ArgNumber { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } for _, cell := range cells.ToList() { - if num := cell.ToNumber(); num.Type == ArgNumber { - values = append(values, num.Number) + if cell.Type == ArgNumber { + values = append(values, cell.Number) } } } @@ -9700,8 +9716,8 @@ func (fn *formulaFuncs) kth(name string, argsList *list.List) formulaArg { } var data []float64 for _, arg := range array { - if numArg := arg.ToNumber(); numArg.Type == ArgNumber { - data = append(data, numArg.Number) + if arg.Type == ArgNumber { + data = append(data, arg.Number) } } if len(data) < k { @@ -9776,25 +9792,10 @@ func (fn *formulaFuncs) MAXIFS(argsList *list.List) formulaArg { // calcListMatrixMax is part of the implementation max. func calcListMatrixMax(maxa bool, max float64, arg formulaArg) float64 { - for _, row := range arg.ToList() { - switch row.Type { - case ArgString: - if !maxa && (row.Value() == "TRUE" || row.Value() == "FALSE") { - continue - } else { - num := row.ToBool() - if num.Type == ArgNumber && num.Number > max { - max = num.Number - continue - } - } - num := row.ToNumber() - if num.Type != ArgError && num.Number > max { - max = num.Number - } - case ArgNumber: - if row.Number > max { - max = row.Number + for _, cell := range arg.ToList() { + if cell.Type == ArgNumber && cell.Number > max { + if maxa && cell.Boolean || !cell.Boolean { + max = cell.Number } } } @@ -9846,33 +9847,31 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "MEDIAN requires at least 1 argument") } var values []float64 - var median, digits float64 - var err error + var median float64 for token := argsList.Front(); token != nil; token = token.Next() { arg := token.Value.(formulaArg) switch arg.Type { case ArgString: - num := arg.ToNumber() - if num.Type == ArgError { - return newErrorFormulaArg(formulaErrorVALUE, num.Error) + value := arg.ToNumber() + if value.Type != ArgNumber { + return value } - values = append(values, num.Number) + values = append(values, value.Number) case ArgNumber: values = append(values, arg.Number) case ArgMatrix: for _, row := range arg.Matrix { - for _, value := range row { - if value.String == "" { - continue - } - if digits, err = strconv.ParseFloat(value.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + for _, cell := range row { + if cell.Type == ArgNumber { + values = append(values, cell.Number) } - values = append(values, digits) } } } } + if len(values) == 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } sort.Float64s(values) if len(values)%2 == 0 { median = (values[len(values)/2-1] + values[len(values)/2]) / 2 @@ -9936,25 +9935,10 @@ func (fn *formulaFuncs) MINIFS(argsList *list.List) formulaArg { // calcListMatrixMin is part of the implementation min. func calcListMatrixMin(mina bool, min float64, arg formulaArg) float64 { - for _, row := range arg.ToList() { - switch row.Type { - case ArgString: - if !mina && (row.Value() == "TRUE" || row.Value() == "FALSE") { - continue - } else { - num := row.ToBool() - if num.Type == ArgNumber && num.Number < min { - min = num.Number - continue - } - } - num := row.ToNumber() - if num.Type != ArgError && num.Number < min { - min = num.Number - } - case ArgNumber: - if row.Number < min { - min = row.Number + for _, cell := range arg.ToList() { + if cell.Type == ArgNumber && cell.Number < min { + if mina && cell.Boolean || !cell.Boolean { + min = cell.Number } } } @@ -10016,7 +10000,7 @@ func (fn *formulaFuncs) pearsonProduct(name string, argsList *list.List) formula } var sum, deltaX, deltaY, x, y, length float64 for i := 0; i < len(array1); i++ { - num1, num2 := array1[i].ToNumber(), array2[i].ToNumber() + num1, num2 := array1[i], array2[i] if !(num1.Type == ArgNumber && num2.Type == ArgNumber) { continue } @@ -10027,7 +10011,7 @@ func (fn *formulaFuncs) pearsonProduct(name string, argsList *list.List) formula x /= length y /= length for i := 0; i < len(array1); i++ { - num1, num2 := array1[i].ToNumber(), array2[i].ToNumber() + num1, num2 := array1[i], array2[i] if !(num1.Type == ArgNumber && num2.Type == ArgNumber) { continue } @@ -10077,9 +10061,8 @@ func (fn *formulaFuncs) PERCENTILEdotEXC(argsList *list.List) formulaArg { if arg.Type == ArgError { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - num := arg.ToNumber() - if num.Type == ArgNumber { - numbers = append(numbers, num.Number) + if arg.Type == ArgNumber { + numbers = append(numbers, arg.Number) } } cnt := len(numbers) @@ -10125,9 +10108,8 @@ func (fn *formulaFuncs) PERCENTILE(argsList *list.List) formulaArg { if arg.Type == ArgError { return arg } - num := arg.ToNumber() - if num.Type == ArgNumber { - numbers = append(numbers, num.Number) + if arg.Type == ArgNumber { + numbers = append(numbers, arg.Number) } } cnt := len(numbers) @@ -10156,11 +10138,10 @@ func (fn *formulaFuncs) percentrank(name string, argsList *list.List) formulaArg var numbers []float64 for _, arg := range array { if arg.Type == ArgError { - return arg + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - num := arg.ToNumber() - if num.Type == ArgNumber { - numbers = append(numbers, num.Number) + if arg.Type == ArgNumber { + numbers = append(numbers, arg.Number) } } cnt := len(numbers) @@ -10350,9 +10331,8 @@ func (fn *formulaFuncs) rank(name string, argsList *list.List) formulaArg { } var arr []float64 for _, arg := range argsList.Front().Next().Value.(formulaArg).ToList() { - n := arg.ToNumber() - if n.Type == ArgNumber { - arr = append(arr, n.Number) + if arg.Type == ArgNumber { + arr = append(arr, arg.Number) } } sort.Float64s(arr) @@ -10422,12 +10402,11 @@ func (fn *formulaFuncs) skew(name string, argsList *list.List) formulaArg { summer += math.Pow((num.Number-mean.Number)/stdDev.Number, 3) count++ case ArgList, ArgMatrix: - for _, row := range token.ToList() { - numArg := row.ToNumber() - if numArg.Type != ArgNumber { + for _, cell := range token.ToList() { + if cell.Type != ArgNumber { continue } - summer += math.Pow((numArg.Number-mean.Number)/stdDev.Number, 3) + summer += math.Pow((cell.Number-mean.Number)/stdDev.Number, 3) count++ } } @@ -10558,7 +10537,7 @@ func (fn *formulaFuncs) STEYX(argsList *list.List) formulaArg { } var count, sumX, sumY, squareX, squareY, sigmaXY float64 for i := 0; i < len(array1); i++ { - num1, num2 := array1[i].ToNumber(), array2[i].ToNumber() + num1, num2 := array1[i], array2[i] if !(num1.Type == ArgNumber && num2.Type == ArgNumber) { continue } @@ -10804,8 +10783,7 @@ func tTest(bTemplin bool, mtx1, mtx2 [][]formulaArg, c1, c2, r1, r2 int) (float6 var fVal formulaArg for i := 0; i < c1; i++ { for j := 0; j < r1; j++ { - fVal = mtx1[i][j].ToNumber() - if fVal.Type == ArgNumber { + if fVal = mtx1[i][j]; fVal.Type == ArgNumber { sum1 += fVal.Number sumSqr1 += fVal.Number * fVal.Number cnt1++ @@ -10814,8 +10792,7 @@ func tTest(bTemplin bool, mtx1, mtx2 [][]formulaArg, c1, c2, r1, r2 int) (float6 } for i := 0; i < c2; i++ { for j := 0; j < r2; j++ { - fVal = mtx2[i][j].ToNumber() - if fVal.Type == ArgNumber { + if fVal = mtx2[i][j]; fVal.Type == ArgNumber { sum2 += fVal.Number sumSqr2 += fVal.Number * fVal.Number cnt2++ @@ -10851,7 +10828,7 @@ func (fn *formulaFuncs) tTest(mtx1, mtx2 [][]formulaArg, fTails, fTyp float64) f var fVal1, fVal2 formulaArg for i := 0; i < c1; i++ { for j := 0; j < r1; j++ { - fVal1, fVal2 = mtx1[i][j].ToNumber(), mtx2[i][j].ToNumber() + fVal1, fVal2 = mtx1[i][j], mtx2[i][j] if fVal1.Type != ArgNumber || fVal2.Type != ArgNumber { continue } @@ -10895,11 +10872,11 @@ func (fn *formulaFuncs) TTEST(argsList *list.List) formulaArg { var array1, array2, tails, typeArg formulaArg array1 = argsList.Front().Value.(formulaArg) array2 = argsList.Front().Next().Value.(formulaArg) - if tails = argsList.Front().Next().Next().Value.(formulaArg).ToNumber(); tails.Type != ArgNumber { - return tails + if tails = argsList.Front().Next().Next().Value.(formulaArg); tails.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - if typeArg = argsList.Back().Value.(formulaArg).ToNumber(); typeArg.Type != ArgNumber { - return typeArg + if typeArg = argsList.Back().Value.(formulaArg); typeArg.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } if len(array1.Matrix) == 0 || len(array2.Matrix) == 0 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) @@ -10944,11 +10921,10 @@ func (fn *formulaFuncs) TRIMMEAN(argsList *list.List) formulaArg { var arr []float64 arrArg := argsList.Front().Value.(formulaArg).ToList() for _, cell := range arrArg { - num := cell.ToNumber() - if num.Type != ArgNumber { + if cell.Type != ArgNumber { continue } - arr = append(arr, num.Number) + arr = append(arr, cell.Number) } discard := math.Floor(float64(len(arr)) * percent.Number / 2) sort.Float64s(arr) @@ -11184,16 +11160,12 @@ func (fn *formulaFuncs) ISBLANK(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "ISBLANK requires 1 argument") } token := argsList.Front().Value.(formulaArg) - result := "FALSE" switch token.Type { - case ArgUnknown: - result = "TRUE" - case ArgString: - if token.String == "" { - result = "TRUE" - } + case ArgUnknown, ArgEmpty: + return newBoolFormulaArg(true) + default: + return newBoolFormulaArg(false) } - return newStringFormulaArg(result) } // ISERR function tests if an initial supplied expression (or value) returns @@ -11256,21 +11228,22 @@ func (fn *formulaFuncs) ISEVEN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISEVEN requires 1 argument") } - var ( - token = argsList.Front().Value.(formulaArg) - result = "FALSE" - numeric int - err error - ) - if token.Type == ArgString { - if numeric, err = strconv.Atoi(token.String); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + token := argsList.Front().Value.(formulaArg) + switch token.Type { + case ArgEmpty: + return newBoolFormulaArg(true) + case ArgNumber, ArgString: + num := token.ToNumber() + if num.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - if numeric == numeric/2*2 { - return newStringFormulaArg("TRUE") + if num.Number == 1 { + return newBoolFormulaArg(false) } + return newBoolFormulaArg(num.Number == num.Number/2*2) + default: + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - return newStringFormulaArg(result) } // ISFORMULA function tests if a specified cell contains a formula, and if so, @@ -11335,12 +11308,10 @@ func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISNONTEXT requires 1 argument") } - token := argsList.Front().Value.(formulaArg) - result := "TRUE" - if token.Type == ArgString && token.String != "" { - result = "FALSE" + if argsList.Front().Value.(formulaArg).Type == ArgString { + return newBoolFormulaArg(false) } - return newStringFormulaArg(result) + return newBoolFormulaArg(true) } // ISNUMBER function tests if a supplied value is a number. If so, @@ -11352,13 +11323,10 @@ func (fn *formulaFuncs) ISNUMBER(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISNUMBER requires 1 argument") } - token, result := argsList.Front().Value.(formulaArg), false - if token.Type == ArgString && token.String != "" { - if _, err := strconv.Atoi(token.String); err == nil { - result = true - } + if argsList.Front().Value.(formulaArg).Type == ArgNumber { + return newBoolFormulaArg(true) } - return newBoolFormulaArg(result) + return newBoolFormulaArg(false) } // ISODD function tests if a supplied number (or numeric expression) evaluates @@ -11370,21 +11338,14 @@ func (fn *formulaFuncs) ISODD(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISODD requires 1 argument") } - var ( - token = argsList.Front().Value.(formulaArg) - result = "FALSE" - numeric int - err error - ) - if token.Type == ArgString { - if numeric, err = strconv.Atoi(token.String); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) - } - if numeric != numeric/2*2 { - return newStringFormulaArg("TRUE") - } + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - return newStringFormulaArg(result) + if int(arg.Number) != int(arg.Number)/2*2 { + return newBoolFormulaArg(true) + } + return newBoolFormulaArg(false) } // ISREF function tests if a supplied value is a reference. If so, the @@ -11524,13 +11485,12 @@ func (fn *formulaFuncs) TYPE(argsList *list.List) formulaArg { return newNumberFormulaArg(16) case ArgMatrix: return newNumberFormulaArg(64) - default: - if arg := token.ToNumber(); arg.Type != ArgError || len(token.Value()) == 0 { - return newNumberFormulaArg(1) - } - if arg := token.ToBool(); arg.Type != ArgError { + case ArgNumber, ArgEmpty: + if token.Boolean { return newNumberFormulaArg(4) } + return newNumberFormulaArg(1) + default: return newNumberFormulaArg(2) } } @@ -13734,9 +13694,9 @@ func (fn *formulaFuncs) TEXTJOIN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "TEXTJOIN accepts at most 252 arguments") } delimiter := argsList.Front().Value.(formulaArg) - ignoreEmpty := argsList.Front().Next().Value.(formulaArg).ToBool() - if ignoreEmpty.Type != ArgNumber { - return ignoreEmpty + ignoreEmpty := argsList.Front().Next().Value.(formulaArg) + if ignoreEmpty.Type != ArgNumber || !ignoreEmpty.Boolean { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } args, ok := textJoin(argsList.Front().Next().Next(), []string{}, ignoreEmpty.Number != 0) if ok.Type != ArgNumber { @@ -13755,7 +13715,7 @@ func textJoin(arg *list.Element, arr []string, ignoreEmpty bool) ([]string, form switch arg.Value.(formulaArg).Type { case ArgError: return arr, arg.Value.(formulaArg) - case ArgString: + case ArgString, ArgEmpty: val := arg.Value.(formulaArg).Value() if val != "" || !ignoreEmpty { arr = append(arr, val) @@ -14040,7 +14000,7 @@ func matchPattern(pattern, name string) (matched bool) { // match, and make compare result as formula criteria condition type. func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte { if lhs.Type != rhs.Type { - return criteriaErr + return criteriaNe } switch lhs.Type { case ArgNumber: @@ -14068,8 +14028,9 @@ func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte return compareFormulaArgList(lhs, rhs, matchMode, caseSensitive) case ArgMatrix: return compareFormulaArgMatrix(lhs, rhs, matchMode, caseSensitive) + default: + return criteriaErr } - return criteriaErr } // compareFormulaArgList compares the left-hand sides and the right-hand sides @@ -14247,8 +14208,8 @@ func checkHVLookupArgs(name string, argsList *list.List) (idx int, lookupValue, errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires second argument of table array", name)) return } - arg := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() - if arg.Type != ArgNumber { + arg := argsList.Front().Next().Next().Value.(formulaArg) + if arg.Type != ArgNumber || arg.Boolean { errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires numeric %s argument", name, unit)) return } @@ -14256,7 +14217,7 @@ func checkHVLookupArgs(name string, argsList *list.List) (idx int, lookupValue, if argsList.Len() == 4 { rangeLookup := argsList.Back().Value.(formulaArg).ToBool() if rangeLookup.Type == ArgError { - errArg = newErrorFormulaArg(formulaErrorVALUE, rangeLookup.Error) + errArg = rangeLookup return } if rangeLookup.Number == 0 { @@ -14442,6 +14403,8 @@ start: } } else if lookupValue.Type == ArgMatrix { lhs = lookupArray + } else if lookupArray.Type == ArgString { + lhs = newStringFormulaArg(cell.Value()) } if compareFormulaArg(lhs, lookupValue, matchMode, false) == criteriaEq { matchIdx = i @@ -14512,6 +14475,8 @@ func lookupBinarySearch(vertical bool, lookupValue, lookupArray, matchMode, sear } } else if lookupValue.Type == ArgMatrix && vertical { lhs = lookupArray + } else if lookupValue.Type == ArgString { + lhs = newStringFormulaArg(cell.Value()) } result := compareFormulaArg(lhs, lookupValue, matchMode, false) if result == criteriaEq { @@ -14524,7 +14489,7 @@ func lookupBinarySearch(vertical bool, lookupValue, lookupArray, matchMode, sear high = mid - 1 } else if result == criteriaL { matchIdx = mid - if lhs.Value() != "" { + if cell.Type != ArgEmpty { lastMatchIdx = matchIdx } low = mid + 1 diff --git a/calc_test.go b/calc_test.go index 9ebfef81c5..5e87763ea7 100644 --- a/calc_test.go +++ b/calc_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/xuri/efp" ) func prepareCalcData(cellData [][]interface{}) *File { @@ -572,6 +573,7 @@ func TestCalcCellValue(t *testing.T) { "=FLOOR(-26.75,-0.1)": "-26.7", "=FLOOR(-26.75,-1)": "-26", "=FLOOR(-26.75,-5)": "-25", + "=FLOOR(-2.05,2)": "-4", "=FLOOR(FLOOR(26.75,1),1)": "26", // _xlfn.FLOOR.MATH "=_xlfn.FLOOR.MATH(58.55)": "58", @@ -706,8 +708,8 @@ func TestCalcCellValue(t *testing.T) { "=POWER(4,POWER(1,1))": "4", // PRODUCT "=PRODUCT(3,6)": "18", - `=PRODUCT("",3,6)`: "18", - `=PRODUCT(PRODUCT(1),3,6)`: "18", + "=PRODUCT(\"3\",\"6\")": "18", + "=PRODUCT(PRODUCT(1),3,6)": "18", "=PRODUCT(C1:C2)": "1", // QUOTIENT "=QUOTIENT(5,2)": "2", @@ -836,7 +838,8 @@ func TestCalcCellValue(t *testing.T) { "=SUBTOTAL(111,A1:A6,A1:A6)": "1.25", // SUM "=SUM(1,2)": "3", - `=SUM("",1,2)`: "3", + "=SUM(\"1\",\"2\")": "3", + "=SUM(\"\",1,2)": "3", "=SUM(1,2+3)": "6", "=SUM(SUM(1,2),2)": "5", "=(-2-SUM(-4+7))*5": "-25", @@ -874,11 +877,12 @@ func TestCalcCellValue(t *testing.T) { "=SUMPRODUCT(A1:B3)": "15", "=SUMPRODUCT(A1:A3,B1:B3,B2:B4)": "20", // SUMSQ - "=SUMSQ(A1:A4)": "14", - "=SUMSQ(A1,B1,A2,B2,6)": "82", - `=SUMSQ("",A1,B1,A2,B2,6)`: "82", - `=SUMSQ(1,SUMSQ(1))`: "2", - "=SUMSQ(MUNIT(3))": "3", + "=SUMSQ(A1:A4)": "14", + "=SUMSQ(A1,B1,A2,B2,6)": "82", + "=SUMSQ(\"\",A1,B1,A2,B2,6)": "82", + "=SUMSQ(1,SUMSQ(1))": "2", + "=SUMSQ(\"1\",SUMSQ(1))": "2", + "=SUMSQ(MUNIT(3))": "3", // SUMX2MY2 "=SUMX2MY2(A1:A4,B1:B4)": "-36", // SUMX2PY2 @@ -914,6 +918,7 @@ func TestCalcCellValue(t *testing.T) { // AVERAGEA "=AVERAGEA(INT(1))": "1", "=AVERAGEA(A1)": "1", + "=AVERAGEA(\"1\")": "1", "=AVERAGEA(A1:A2)": "1.5", "=AVERAGEA(D2:F9)": "12671.375", // BETA.DIST @@ -1013,6 +1018,7 @@ func TestCalcCellValue(t *testing.T) { "=COUNTA()": "0", "=COUNTA(A1:A5,B2:B5,\"text\",1,INT(2))": "8", "=COUNTA(COUNTA(1),MUNIT(1))": "2", + "=COUNTA(D1:D2)": "2", // COUNTBLANK "=COUNTBLANK(MUNIT(1))": "0", "=COUNTBLANK(1)": "0", @@ -1074,10 +1080,11 @@ func TestCalcCellValue(t *testing.T) { "=GAMMALN.PRECISE(0.4)": "0.796677817701784", "=GAMMALN.PRECISE(4.5)": "2.45373657084244", // GAUSS - "=GAUSS(-5)": "-0.499999713348428", - "=GAUSS(0)": "0", - "=GAUSS(0.1)": "0.039827837277029", - "=GAUSS(2.5)": "0.493790334674224", + "=GAUSS(-5)": "-0.499999713348428", + "=GAUSS(0)": "0", + "=GAUSS(\"0\")": "0", + "=GAUSS(0.1)": "0.039827837277029", + "=GAUSS(2.5)": "0.493790334674224", // GEOMEAN "=GEOMEAN(2.5,3,0.5,1,3)": "1.6226711115996", // HARMEAN @@ -1373,6 +1380,7 @@ func TestCalcCellValue(t *testing.T) { // ISEVEN "=ISEVEN(A1)": "FALSE", "=ISEVEN(A2)": "TRUE", + "=ISEVEN(G1)": "TRUE", // ISFORMULA "=ISFORMULA(A1)": "FALSE", "=ISFORMULA(\"A\")": "FALSE", @@ -1388,7 +1396,7 @@ func TestCalcCellValue(t *testing.T) { "=ISNA(A1)": "FALSE", "=ISNA(NA())": "TRUE", // ISNONTEXT - "=ISNONTEXT(A1)": "FALSE", + "=ISNONTEXT(A1)": "TRUE", "=ISNONTEXT(A5)": "TRUE", `=ISNONTEXT("Excelize")`: "FALSE", "=ISNONTEXT(NA())": "TRUE", @@ -1421,7 +1429,7 @@ func TestCalcCellValue(t *testing.T) { // TYPE "=TYPE(2)": "1", "=TYPE(10/2)": "1", - "=TYPE(C1)": "1", + "=TYPE(C2)": "1", "=TYPE(\"text\")": "2", "=TYPE(TRUE)": "4", "=TYPE(NA())": "16", @@ -1446,6 +1454,7 @@ func TestCalcCellValue(t *testing.T) { "=IFERROR(1/2,0)": "0.5", "=IFERROR(ISERROR(),0)": "0", "=IFERROR(1/0,0)": "0", + "=IFERROR(G1,2)": "0", "=IFERROR(B2/MROUND(A2,1),0)": "2.5", // IFNA "=IFNA(1,\"not found\")": "1", @@ -1787,16 +1796,17 @@ func TestCalcCellValue(t *testing.T) { "=VALUE(\"01/02/2006 15:04:05\")": "38719.6278356481", // Conditional Functions // IF - "=IF(1=1)": "TRUE", - "=IF(1<>1)": "FALSE", - "=IF(5<0, \"negative\", \"positive\")": "positive", - "=IF(-2<0, \"negative\", \"positive\")": "negative", - `=IF(1=1, "equal", "notequal")`: "equal", - `=IF(1<>1, "equal", "notequal")`: "notequal", - `=IF("A"="A", "equal", "notequal")`: "equal", - `=IF("A"<>"A", "equal", "notequal")`: "notequal", - `=IF(FALSE,0,ROUND(4/2,0))`: "2", - `=IF(TRUE,ROUND(4/2,0),0)`: "2", + "=IF(1=1)": "TRUE", + "=IF(1<>1)": "FALSE", + "=IF(5<0, \"negative\", \"positive\")": "positive", + "=IF(-2<0, \"negative\", \"positive\")": "negative", + "=IF(1=1, \"equal\", \"notequal\")": "equal", + "=IF(1<>1, \"equal\", \"notequal\")": "notequal", + "=IF(\"A\"=\"A\", \"equal\", \"notequal\")": "equal", + "=IF(\"A\"<>\"A\", \"equal\", \"notequal\")": "notequal", + "=IF(FALSE,0,ROUND(4/2,0))": "2", + "=IF(TRUE,ROUND(4/2,0),0)": "2", + "=IF(A4>0.4,\"TRUE\",\"FALSE\")": "FALSE", // Excel Lookup and Reference Functions // ADDRESS "=ADDRESS(1,1,1,TRUE)": "$A$1", @@ -1855,6 +1865,7 @@ func TestCalcCellValue(t *testing.T) { "=VLOOKUP(INT(F2),F3:F9,1,TRUE)": "32080", "=VLOOKUP(MUNIT(3),MUNIT(3),1)": "0", "=VLOOKUP(A1,A3:B5,1)": "0", + "=VLOOKUP(A1:A2,A1:A1,1)": "1", "=VLOOKUP(MUNIT(1),MUNIT(1),1,FALSE)": "1", // INDEX "=INDEX(0,0,0)": "0", @@ -2556,13 +2567,13 @@ func TestCalcCellValue(t *testing.T) { "=MDETERM()": "MDETERM requires 1 argument", // MINVERSE "=MINVERSE()": "MINVERSE requires 1 argument", - "=MINVERSE(B3:C4)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=MINVERSE(B3:C4)": "#VALUE!", "=MINVERSE(A1:C2)": "#VALUE!", "=MINVERSE(A4:A4)": "#NUM!", // MMULT "=MMULT()": "MMULT requires 2 argument", - "=MMULT(A1:B2,B3:C4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MMULT(B3:C4,A1:B2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=MMULT(A1:B2,B3:C4)": "#VALUE!", + "=MMULT(B3:C4,A1:B2)": "#VALUE!", "=MMULT(A1:A2,B1:B2)": "#VALUE!", // MOD "=MOD()": "MOD requires 2 numeric arguments", @@ -2593,7 +2604,8 @@ func TestCalcCellValue(t *testing.T) { "=POWER(0,-1)": "#DIV/0!", "=POWER(1)": "POWER requires 2 numeric arguments", // PRODUCT - `=PRODUCT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=PRODUCT(\"X\")": "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=PRODUCT(\"\",3,6)": "strconv.ParseFloat: parsing \"\": invalid syntax", // QUOTIENT `=QUOTIENT("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", `=QUOTIENT(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -2697,6 +2709,7 @@ func TestCalcCellValue(t *testing.T) { "=SUMPRODUCT(A1,D1)": "#VALUE!", "=SUMPRODUCT(A1:A3,D1:D3)": "#VALUE!", "=SUMPRODUCT(A1:A2,B1:B3)": "#VALUE!", + "=SUMPRODUCT(\"\")": "#VALUE!", "=SUMPRODUCT(A1,NA())": "#N/A", // SUMX2MY2 "=SUMX2MY2()": "SUMX2MY2 requires 2 arguments", @@ -2922,6 +2935,7 @@ func TestCalcCellValue(t *testing.T) { // FISHER "=FISHER()": "FISHER requires 1 numeric argument", "=FISHER(2)": "#N/A", + "=FISHER(\"2\")": "#N/A", "=FISHER(INT(-2)))": "#N/A", "=FISHER(F1)": "FISHER requires 1 numeric argument", // FISHERINV @@ -2984,7 +2998,8 @@ func TestCalcCellValue(t *testing.T) { // GEOMEAN "=GEOMEAN()": "GEOMEAN requires at least 1 numeric argument", "=GEOMEAN(0)": "#NUM!", - "=GEOMEAN(D1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", + "=GEOMEAN(D1:D2)": "#NUM!", + "=GEOMEAN(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // HARMEAN "=HARMEAN()": "HARMEAN requires at least 1 argument", "=HARMEAN(-1)": "#N/A", @@ -3184,7 +3199,7 @@ func TestCalcCellValue(t *testing.T) { // MEDIAN "=MEDIAN()": "MEDIAN requires at least 1 argument", "=MEDIAN(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MEDIAN(D1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", + "=MEDIAN(D1:D2)": "#NUM!", // MIN "=MIN()": "MIN requires at least 1 argument", "=MIN(NA())": "#N/A", @@ -3407,8 +3422,9 @@ func TestCalcCellValue(t *testing.T) { // ISERROR "=ISERROR()": "ISERROR requires 1 argument", // ISEVEN - "=ISEVEN()": "ISEVEN requires 1 argument", - `=ISEVEN("text")`: "strconv.Atoi: parsing \"text\": invalid syntax", + "=ISEVEN()": "ISEVEN requires 1 argument", + "=ISEVEN(\"text\")": "#VALUE!", + "=ISEVEN(A1:A2)": "#VALUE!", // ISFORMULA "=ISFORMULA()": "ISFORMULA requires 1 argument", // ISLOGICAL @@ -3420,8 +3436,8 @@ func TestCalcCellValue(t *testing.T) { // ISNUMBER "=ISNUMBER()": "ISNUMBER requires 1 argument", // ISODD - "=ISODD()": "ISODD requires 1 argument", - `=ISODD("text")`: "strconv.Atoi: parsing \"text\": invalid syntax", + "=ISODD()": "ISODD requires 1 argument", + "=ISODD(\"text\")": "#VALUE!", // ISREF "=ISREF()": "ISREF requires 1 argument", // ISTEXT @@ -3717,7 +3733,7 @@ func TestCalcCellValue(t *testing.T) { "=SUBSTITUTE(\"\",\"\",\"\",0)": "instance_num should be > 0", // TEXTJOIN "=TEXTJOIN()": "TEXTJOIN requires at least 3 arguments", - "=TEXTJOIN(\"\",\"\",1)": "strconv.ParseBool: parsing \"\": invalid syntax", + "=TEXTJOIN(\"\",\"\",1)": "#VALUE!", "=TEXTJOIN(\"\",TRUE,NA())": "#N/A", "=TEXTJOIN(\"\",TRUE," + strings.Repeat("0,", 250) + ",0)": "TEXTJOIN accepts at most 252 arguments", "=TEXTJOIN(\",\",FALSE,REPT(\"*\",32768))": "TEXTJOIN function exceeds 32767 characters", @@ -3804,7 +3820,6 @@ func TestCalcCellValue(t *testing.T) { "=VLOOKUP(D2,D1,1,FALSE)": "VLOOKUP requires second argument of table array", "=VLOOKUP(D2,D:D,FALSE,FALSE)": "VLOOKUP requires numeric col argument", "=VLOOKUP(D2,D:D,1,FALSE,FALSE)": "VLOOKUP requires at most 4 arguments", - "=VLOOKUP(A1:A2,A1:A1,1)": "VLOOKUP no result found", "=VLOOKUP(D2,D10:D10,1,FALSE)": "VLOOKUP no result found", "=VLOOKUP(D2,D:D,2,FALSE)": "VLOOKUP has invalid column index", "=VLOOKUP(D2,C:C,1,FALSE)": "VLOOKUP no result found", @@ -4455,7 +4470,7 @@ func TestCalcISBLANK(t *testing.T) { }) fn := formulaFuncs{} result := fn.ISBLANK(argsList) - assert.Equal(t, result.String, "TRUE") + assert.Equal(t, "TRUE", result.Value()) assert.Empty(t, result.Error) } @@ -4520,6 +4535,7 @@ func TestCalcMatchPattern(t *testing.T) { assert.True(t, matchPattern("", "")) assert.True(t, matchPattern("file/*", "file/abc/bcd/def")) assert.True(t, matchPattern("*", "")) + assert.False(t, matchPattern("?", "")) assert.False(t, matchPattern("file/?", "file/abc/bcd/def")) } @@ -4574,15 +4590,14 @@ func TestCalcVLOOKUP(t *testing.T) { } func TestCalcBoolean(t *testing.T) { - cellData := [][]interface{}{ - {0.5, "TRUE", -0.5, "FALSE"}, - } + cellData := [][]interface{}{{0.5, "TRUE", -0.5, "FALSE", true}} f := prepareCalcData(cellData) formulaList := map[string]string{ "=AVERAGEA(A1:C1)": "0.333333333333333", "=MAX(0.5,B1)": "0.5", "=MAX(A1:B1)": "0.5", - "=MAXA(A1:B1)": "1", + "=MAXA(A1:B1)": "0.5", + "=MAXA(A1:E1)": "1", "=MAXA(0.5,B1)": "1", "=MIN(-0.5,D1)": "-0.5", "=MIN(C1:D1)": "-0.5", @@ -4600,6 +4615,23 @@ func TestCalcBoolean(t *testing.T) { } } +func TestCalcMAXMIN(t *testing.T) { + cellData := [][]interface{}{{"1"}, {"2"}, {true}} + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=MAX(A1:A3)": "0", + "=MAXA(A1:A3)": "1", + "=MIN(A1:A3)": "0", + "=MINA(A1:A3)": "1", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", formula)) + result, err := f.CalcCellValue("Sheet1", "B1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } +} + func TestCalcAVERAGEIF(t *testing.T) { f := prepareCalcData([][]interface{}{ {"Monday", 500}, @@ -4822,28 +4854,29 @@ func TestCalcGROWTHandTREND(t *testing.T) { calcError := map[string]string{ "=GROWTH()": "GROWTH requires at least 1 argument", "=GROWTH(B2:B5,A2:A5,A8:A10,TRUE,0)": "GROWTH allows at most 4 arguments", - "=GROWTH(A1:B1,A2:A5,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", - "=GROWTH(B2:B5,A1:B1,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", - "=GROWTH(B2:B5,A2:A5,A1:B1,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", + "=GROWTH(A1:B1,A2:A5,A8:A10,TRUE)": "#VALUE!", + "=GROWTH(B2:B5,A1:B1,A8:A10,TRUE)": "#VALUE!", + "=GROWTH(B2:B5,A2:A5,A1:B1,TRUE)": "#VALUE!", "=GROWTH(B2:B5,A2:A5,A8:A10,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", "=GROWTH(A2:B3,A4:B4)": "#REF!", "=GROWTH(A4:B4,A2:A2)": "#REF!", "=GROWTH(A2:A2,A4:A5)": "#REF!", - "=GROWTH(C1:C1,A2:A3)": "#NUM!", + "=GROWTH(C1:C1,A2:A3)": "#VALUE!", "=GROWTH(D1:D1,A2:A3)": "#NUM!", - "=GROWTH(A2:A3,C1:C1)": "#NUM!", + "=GROWTH(A2:A3,C1:C1)": "#VALUE!", "=TREND()": "TREND requires at least 1 argument", "=TREND(B2:B5,A2:A5,A8:A10,TRUE,0)": "TREND allows at most 4 arguments", - "=TREND(A1:B1,A2:A5,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", - "=TREND(B2:B5,A1:B1,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", - "=TREND(B2:B5,A2:A5,A1:B1,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", + "=TREND(A1:B1,A2:A5,A8:A10,TRUE)": "#VALUE!", + "=TREND(B2:B5,A1:B1,A8:A10,TRUE)": "#VALUE!", + "=TREND(B2:B5,A2:A5,A1:B1,TRUE)": "#VALUE!", "=TREND(B2:B5,A2:A5,A8:A10,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", "=TREND(A2:B3,A4:B4)": "#REF!", "=TREND(A4:B4,A2:A2)": "#REF!", "=TREND(A2:A2,A4:A5)": "#REF!", - "=TREND(C1:C1,A2:A3)": "#NUM!", + "=TREND(C1:C1,A2:A3)": "#VALUE!", "=TREND(D1:D1,A2:A3)": "#REF!", - "=TREND(A2:A3,C1:C1)": "#NUM!", + "=TREND(A2:A3,C1:C1)": "#VALUE!", + "=TREND(C1:C1,C1:C1)": "#VALUE!", } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) @@ -5586,8 +5619,8 @@ func TestCalcTTEST(t *testing.T) { "=TTEST()": "TTEST requires 4 arguments", "=TTEST(\"\",B1:B12,1,1)": "#NUM!", "=TTEST(A1:A12,\"\",1,1)": "#NUM!", - "=TTEST(A1:A12,B1:B12,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=TTEST(A1:A12,B1:B12,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TTEST(A1:A12,B1:B12,\"\",1)": "#VALUE!", + "=TTEST(A1:A12,B1:B12,1,\"\")": "#VALUE!", "=TTEST(A1:A12,B1:B12,0,1)": "#NUM!", "=TTEST(A1:A12,B1:B12,1,0)": "#NUM!", "=TTEST(A1:A2,B1:B1,1,1)": "#N/A", @@ -5598,8 +5631,8 @@ func TestCalcTTEST(t *testing.T) { "=T.TEST()": "T.TEST requires 4 arguments", "=T.TEST(\"\",B1:B12,1,1)": "#NUM!", "=T.TEST(A1:A12,\"\",1,1)": "#NUM!", - "=T.TEST(A1:A12,B1:B12,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.TEST(A1:A12,B1:B12,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.TEST(A1:A12,B1:B12,\"\",1)": "#VALUE!", + "=T.TEST(A1:A12,B1:B12,1,\"\")": "#VALUE!", "=T.TEST(A1:A12,B1:B12,0,1)": "#NUM!", "=T.TEST(A1:A12,B1:B12,1,0)": "#NUM!", "=T.TEST(A1:A2,B1:B1,1,1)": "#N/A", @@ -5618,8 +5651,8 @@ func TestCalcTTEST(t *testing.T) { func TestCalcNETWORKDAYSandWORKDAY(t *testing.T) { cellData := [][]interface{}{ - {"05/01/2019", 43586}, - {"09/13/2019", 43721}, + {"05/01/2019", 43586, "text1"}, + {"09/13/2019", 43721, "text2"}, {"10/01/2019", 43739}, {"12/25/2019", 43824}, {"01/01/2020", 43831}, @@ -5651,6 +5684,7 @@ func TestCalcNETWORKDAYSandWORKDAY(t *testing.T) { "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",17)": "219", "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,A1:A12)": "178", "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,B1:B12)": "178", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,C1:C2)": "183", "=WORKDAY(\"12/01/2015\",25)": "42374", "=WORKDAY(\"01/01/2020\",123,B1:B12)": "44006", "=WORKDAY.INTL(\"12/01/2015\",0)": "42339", @@ -5813,3 +5847,27 @@ func TestNestedFunctionsWithOperators(t *testing.T) { assert.Equal(t, expected, result, formula) } } + +func TestFormulaArgToToken(t *testing.T) { + assert.Equal(t, + efp.Token{ + TType: efp.TokenTypeOperand, + TSubType: efp.TokenSubTypeLogical, + TValue: "TRUE", + }, + formulaArgToToken(newBoolFormulaArg(true)), + ) +} + +func TestPrepareTrendGrowth(t *testing.T) { + assert.Equal(t, [][]float64(nil), prepareTrendGrowthMtxX([][]float64{{0, 0}, {0, 0}})) + assert.Equal(t, [][]float64(nil), prepareTrendGrowthMtxY(false, [][]float64{{0, 0}, {0, 0}})) + info, err := prepareTrendGrowth(false, [][]float64{{0, 0}, {0, 0}}, [][]float64{{0, 0}, {0, 0}}) + assert.Nil(t, info) + assert.Equal(t, newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM), err) +} + +func TestCalcColRowQRDecomposition(t *testing.T) { + assert.False(t, calcRowQRDecomposition([][]float64{{0, 0}, {0, 0}}, []float64{0, 0}, 1, 0)) + assert.False(t, calcColQRDecomposition([][]float64{{0, 0}, {0, 0}}, []float64{0, 0}, 1, 0)) +} diff --git a/cell.go b/cell.go index 992a7412ac..caae77477f 100644 --- a/cell.go +++ b/cell.go @@ -519,8 +519,12 @@ func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) { // string. func (c *xlsxC) setCellDefault(value string) { if ok, _, _ := isNumeric(value); !ok { - c.setInlineStr(value) - c.IS.T.Val = value + if value != "" { + c.setInlineStr(value) + c.IS.T.Val = value + return + } + c.T, c.V, c.IS = value, value, nil return } c.V = value diff --git a/excelize_test.go b/excelize_test.go index a1857fe4dd..63d96f4f16 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -253,7 +253,8 @@ func TestOpenReader(t *testing.T) { for _, defaultXMLPath := range []string{ defaultXMLPathCalcChain, defaultXMLPathStyles, - defaultXMLPathWorkbookRels} { + defaultXMLPathWorkbookRels, + } { _, err = OpenReader(preset(defaultXMLPath)) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } diff --git a/go.mod b/go.mod index aaa5a82421..b6c63e8b43 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/stretchr/testify v1.8.0 github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 - golang.org/x/crypto v0.4.0 + golang.org/x/crypto v0.5.0 golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 - golang.org/x/net v0.4.0 - golang.org/x/text v0.5.0 + golang.org/x/net v0.5.0 + golang.org/x/text v0.6.0 ) require github.com/richardlehane/msoleps v1.0.3 // indirect diff --git a/go.sum b/go.sum index 65f6bfd63a..7e2848d986 100644 --- a/go.sum +++ b/go.sum @@ -22,17 +22,16 @@ github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -40,15 +39,15 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/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/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 14d7acd97eaf100ffbdad4b82317e38858af9478 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 10 Jan 2023 01:02:48 +0800 Subject: [PATCH 145/213] This fixes #1441, add copyright agreement statement on the LICENSE --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 391f88aede..b9bcc5737f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ BSD 3-Clause License Copyright (c) 2016-2023 The excelize Authors. +Copyright (c) 2011-2017 Geoffrey J. Teale All rights reserved. Redistribution and use in source and binary forms, with or without From 00c58a73f32e1e8e176abee6f775b865c542e52d Mon Sep 17 00:00:00 2001 From: Liron Levin Date: Wed, 11 Jan 2023 18:14:38 +0200 Subject: [PATCH 146/213] Fix panic caused by the workbook relationship part not exist (#1443) - Check nil map in the getSheetMap function - Update unit tests --- excelize_test.go | 30 +++++++++++++++++++++++------- sheet.go | 3 +++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 63d96f4f16..7e19c5b802 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -228,14 +228,18 @@ func TestOpenReader(t *testing.T) { _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password", UnzipXMLSizeLimit: UnzipSizeLimit + 1}) assert.EqualError(t, err, ErrWorkbookFileFormat.Error()) - // Test open workbook with unsupported charset internal calculation chain - preset := func(filePath string) *bytes.Buffer { + // Prepare unusual workbook, made the specified internal XML parts missing + // or contain unsupported charset + preset := func(filePath string, notExist bool) *bytes.Buffer { source, err := zip.OpenReader(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) buf := new(bytes.Buffer) zw := zip.NewWriter(buf) for _, item := range source.File { // The following statements can be simplified as zw.Copy(item) in go1.17 + if notExist && item.Name == filePath { + continue + } writer, err := zw.Create(item.Name) assert.NoError(t, err) readerCloser, err := item.Open() @@ -243,21 +247,33 @@ func TestOpenReader(t *testing.T) { _, err = io.Copy(writer, readerCloser) assert.NoError(t, err) } - fi, err := zw.Create(filePath) - assert.NoError(t, err) - _, err = fi.Write(MacintoshCyrillicCharset) - assert.NoError(t, err) + if !notExist { + fi, err := zw.Create(filePath) + assert.NoError(t, err) + _, err = fi.Write(MacintoshCyrillicCharset) + assert.NoError(t, err) + } assert.NoError(t, zw.Close()) return buf } + // Test open workbook with unsupported charset internal XML parts for _, defaultXMLPath := range []string{ defaultXMLPathCalcChain, defaultXMLPathStyles, defaultXMLPathWorkbookRels, } { - _, err = OpenReader(preset(defaultXMLPath)) + _, err = OpenReader(preset(defaultXMLPath, false)) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } + // Test open workbook without internal XML parts + for _, defaultXMLPath := range []string{ + defaultXMLPathCalcChain, + defaultXMLPathStyles, + defaultXMLPathWorkbookRels, + } { + _, err = OpenReader(preset(defaultXMLPath, true)) + assert.NoError(t, err) + } // Test open spreadsheet with unzip size limit _, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipSizeLimit: 100}) diff --git a/sheet.go b/sheet.go index a4be16a523..9d1f5f9bc4 100644 --- a/sheet.go +++ b/sheet.go @@ -464,6 +464,9 @@ func (f *File) getSheetMap() (map[string]string, error) { if err != nil { return nil, err } + if rels == nil { + return maps, nil + } for _, v := range wb.Sheets.Sheet { for _, rel := range rels.Relationships { if rel.ID == v.ID { From 4f0025aab07bb4cb290c38932619271c2cae7552 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 13 Jan 2023 00:05:46 +0800 Subject: [PATCH 147/213] This closes #1447, add support for strict theme namespace - Support specify if applying number format style for the cell calculation result - Reduce cyclomatic complexities for the OpenReader function --- calc.go | 21 ++++++++---- excelize.go | 29 ++++++++++------ lib.go | 16 +++++---- merge.go | 2 +- xmlDrawing.go | 91 +++++++++++++++++++++++++++------------------------ 5 files changed, 94 insertions(+), 65 deletions(-) diff --git a/calc.go b/calc.go index b864a23e2f..7e1fa71e2d 100644 --- a/calc.go +++ b/calc.go @@ -767,20 +767,29 @@ type formulaFuncs struct { // YIELDMAT // Z.TEST // ZTEST -func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { - var token formulaArg - token, err = f.calcCellValue(&calcContext{ +func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string, err error) { + var ( + rawCellValue = parseOptions(opts...).RawCellValue + styleIdx int + token formulaArg + ) + if token, err = f.calcCellValue(&calcContext{ entry: fmt.Sprintf("%s!%s", sheet, cell), iterations: make(map[string]uint), - }, sheet, cell) + }, sheet, cell); err != nil { + return + } + if !rawCellValue { + styleIdx, _ = f.GetCellStyle(sheet, cell) + } result = token.Value() if isNum, precision, decimal := isNumeric(result); isNum { if precision > 15 { - result = strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64)) + result, err = f.formattedValue(styleIdx, strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64)), rawCellValue) return } if !strings.HasPrefix(result, "0") { - result = strings.ToUpper(strconv.FormatFloat(decimal, 'f', -1, 64)) + result, err = f.formattedValue(styleIdx, strings.ToUpper(strconv.FormatFloat(decimal, 'f', -1, 64)), rawCellValue) } } return diff --git a/excelize.go b/excelize.go index b41cd00a23..5a47f203b7 100644 --- a/excelize.go +++ b/excelize.go @@ -132,15 +132,9 @@ func newFile() *File { } } -// OpenReader read data stream from io.Reader and return a populated -// spreadsheet file. -func OpenReader(r io.Reader, opts ...Options) (*File, error) { - b, err := io.ReadAll(r) - if err != nil { - return nil, err - } - f := newFile() - f.options = parseOptions(opts...) +// checkOpenReaderOptions check and validate options field value for open +// reader. +func (f *File) checkOpenReaderOptions() error { if f.options.UnzipSizeLimit == 0 { f.options.UnzipSizeLimit = UnzipSizeLimit if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit { @@ -154,7 +148,22 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) { } } if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit { - return nil, ErrOptionsUnzipSizeLimit + return ErrOptionsUnzipSizeLimit + } + return nil +} + +// OpenReader read data stream from io.Reader and return a populated +// spreadsheet file. +func OpenReader(r io.Reader, opts ...Options) (*File, error) { + b, err := io.ReadAll(r) + if err != nil { + return nil, err + } + f := newFile() + f.options = parseOptions(opts...) + if err = f.checkOpenReaderOptions(); err != nil { + return nil, err } if bytes.Contains(b, oleIdentifier) { if b, err = Decrypt(b, f.options); err != nil { diff --git a/lib.go b/lib.go index e5637ec9f1..887946aef5 100644 --- a/lib.go +++ b/lib.go @@ -497,12 +497,16 @@ func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err // Transitional namespaces. func namespaceStrictToTransitional(content []byte) []byte { namespaceTranslationDic := map[string]string{ - StrictSourceRelationship: SourceRelationship.Value, - StrictSourceRelationshipOfficeDocument: SourceRelationshipOfficeDocument, - StrictSourceRelationshipChart: SourceRelationshipChart, - StrictSourceRelationshipComments: SourceRelationshipComments, - StrictSourceRelationshipImage: SourceRelationshipImage, - StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet.Value, + StrictNameSpaceDocumentPropertiesVariantTypes: NameSpaceDocumentPropertiesVariantTypes.Value, + StrictNameSpaceDrawingMLMain: NameSpaceDrawingMLMain, + StrictNameSpaceExtendedProperties: NameSpaceExtendedProperties, + StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet.Value, + StrictSourceRelationship: SourceRelationship.Value, + StrictSourceRelationshipChart: SourceRelationshipChart, + StrictSourceRelationshipComments: SourceRelationshipComments, + StrictSourceRelationshipExtendProperties: SourceRelationshipExtendProperties, + StrictSourceRelationshipImage: SourceRelationshipImage, + StrictSourceRelationshipOfficeDocument: SourceRelationshipOfficeDocument, } for s, n := range namespaceTranslationDic { content = bytesReplace(content, []byte(s), []byte(n), -1) diff --git a/merge.go b/merge.go index b3138aff6b..eb3fea30ca 100644 --- a/merge.go +++ b/merge.go @@ -41,7 +41,7 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) { // B1(x1,y1) D1(x2,y1) // +------------------------+ // | | -// A4(x3,y3) | C4(x4,y3) | +// A4(x3,y3) | C4(x4,y3) | // +------------------------+ | // | | | | // | |B5(x1,y2) | D5(x2,y2)| diff --git a/xmlDrawing.go b/xmlDrawing.go index c3c524b59e..30b4d0942e 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -38,48 +38,55 @@ var ( // Source relationship and namespace. const ( - ContentTypeAddinMacro = "application/vnd.ms-excel.addin.macroEnabled.main+xml" - ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml" - ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" - ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml" - ContentTypeSheetML = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" - ContentTypeSpreadSheetMLChartsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" - ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" - ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" - ContentTypeSpreadSheetMLPivotTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" - ContentTypeSpreadSheetMLSharedStrings = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" - ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml" - ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" - ContentTypeTemplate = "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" - ContentTypeTemplateMacro = "application/vnd.ms-excel.template.macroEnabled.main+xml" - ContentTypeVBA = "application/vnd.ms-office.vbaProject" - ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing" - NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" - NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/" - NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" - NameSpaceXML = "http://www.w3.org/XML/1998/namespace" - NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" - SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" - SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" - SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" - SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet" - SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" - SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" - SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" - SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" - SourceRelationshipOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" - SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" - SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" - SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" - SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" - SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" - SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" - StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" - StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" - StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart" - StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments" - StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image" - StrictSourceRelationshipOfficeDocument = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument" + ContentTypeAddinMacro = "application/vnd.ms-excel.addin.macroEnabled.main+xml" + ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml" + ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" + ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + ContentTypeSheetML = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" + ContentTypeSpreadSheetMLChartsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" + ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" + ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" + ContentTypeSpreadSheetMLPivotTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" + ContentTypeSpreadSheetMLSharedStrings = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" + ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml" + ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" + ContentTypeTemplate = "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" + ContentTypeTemplateMacro = "application/vnd.ms-excel.template.macroEnabled.main+xml" + ContentTypeVBA = "application/vnd.ms-office.vbaProject" + ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing" + NameSpaceDrawingMLMain = "http://schemas.openxmlformats.org/drawingml/2006/main" + NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" + NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/" + NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" + NameSpaceExtendedProperties = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" + NameSpaceXML = "http://www.w3.org/XML/1998/namespace" + NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" + SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" + SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" + SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" + SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet" + SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" + SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" + SourceRelationshipExtendProperties = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" + SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" + SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" + SourceRelationshipOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" + SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" + SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" + SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" + SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" + SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" + SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" + StrictNameSpaceDocumentPropertiesVariantTypes = "http://purl.oclc.org/ooxml/officeDocument/docPropsVTypes" + StrictNameSpaceDrawingMLMain = "http://purl.oclc.org/ooxml/drawingml/main" + StrictNameSpaceExtendedProperties = "http://purl.oclc.org/ooxml/officeDocument/extendedProperties" + StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" + StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" + StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart" + StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments" + StrictSourceRelationshipExtendProperties = "http://purl.oclc.org/ooxml/officeDocument/relationships/extendedProperties" + StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image" + StrictSourceRelationshipOfficeDocument = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument" // ExtURIConditionalFormattings is the extLst child element // ([ISO/IEC29500-1:2016] section 18.2.10) of the worksheet element // ([ISO/IEC29500-1:2016] section 18.3.1.99) is extended by the addition of From 917e6e19d60b792d936a2c92e58a82d341b6daff Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 20 Jan 2023 03:10:04 +0000 Subject: [PATCH 148/213] This roundup time value when a millisecond great than 500 to fix the accuracy issue - Correction example in the documentation of set cell formula - Rename the internal function `parseOptions` to `getOptions` - Update unit tests --- calc.go | 2 +- cell.go | 10 +++++----- col.go | 2 +- date.go | 6 +++++- excelize.go | 6 +++--- numfmt_test.go | 6 +++--- rows.go | 2 +- rows_test.go | 5 +++++ 8 files changed, 24 insertions(+), 15 deletions(-) diff --git a/calc.go b/calc.go index 7e1fa71e2d..eff012fdfa 100644 --- a/calc.go +++ b/calc.go @@ -769,7 +769,7 @@ type formulaFuncs struct { // ZTEST func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string, err error) { var ( - rawCellValue = parseOptions(opts...).RawCellValue + rawCellValue = getOptions(opts...).RawCellValue styleIdx int token formulaArg ) diff --git a/cell.go b/cell.go index caae77477f..3005222cfa 100644 --- a/cell.go +++ b/cell.go @@ -71,7 +71,7 @@ func (f *File) GetCellValue(sheet, cell string, opts ...Options) (string, error) if err != nil { return "", true, err } - val, err := c.getValueFrom(f, sst, parseOptions(opts...).RawCellValue) + val, err := c.getValueFrom(f, sst, getOptions(opts...).RawCellValue) return val, true, err }) } @@ -640,12 +640,12 @@ type FormulaOpts struct { // // err := f.SetCellFormula("Sheet1", "A3", "=SUM(A1,B1)") // -// Example 2, set one-dimensional vertical constant array (row array) formula +// Example 2, set one-dimensional vertical constant array (column array) formula // "1,2,3" for the cell "A3" on "Sheet1": // -// err := f.SetCellFormula("Sheet1", "A3", "={1,2,3}") +// err := f.SetCellFormula("Sheet1", "A3", "={1;2;3}") // -// Example 3, set one-dimensional horizontal constant array (column array) +// Example 3, set one-dimensional horizontal constant array (row array) // formula '"a","b","c"' for the cell "A3" on "Sheet1": // // err := f.SetCellFormula("Sheet1", "A3", "={\"a\",\"b\",\"c\"}") @@ -654,7 +654,7 @@ type FormulaOpts struct { // the cell "A3" on "Sheet1": // // formulaType, ref := excelize.STCellFormulaTypeArray, "A3:A3" -// err := f.SetCellFormula("Sheet1", "A3", "={1,2,\"a\",\"b\"}", +// err := f.SetCellFormula("Sheet1", "A3", "={1,2;\"a\",\"b\"}", // excelize.FormulaOpts{Ref: &ref, Type: &formulaType}) // // Example 5, set range array formula "A1:A2" for the cell "A3" on "Sheet1": diff --git a/col.go b/col.go index bb1ffd5ce4..d3852048a7 100644 --- a/col.go +++ b/col.go @@ -92,7 +92,7 @@ func (cols *Cols) Rows(opts ...Options) ([]string, error) { if cols.stashCol >= cols.curCol { return rowIterator.cells, rowIterator.err } - cols.rawCellValue = parseOptions(opts...).RawCellValue + cols.rawCellValue = getOptions(opts...).RawCellValue if cols.sst, rowIterator.err = cols.f.sharedStringsReader(); rowIterator.err != nil { return rowIterator.cells, rowIterator.err } diff --git a/date.go b/date.go index b3cbb75c85..94ce218371 100644 --- a/date.go +++ b/date.go @@ -156,7 +156,11 @@ func timeFromExcelTime(excelTime float64, date1904 bool) time.Time { date = excel1900Epoc } durationPart := time.Duration(nanosInADay * floatPart) - return date.AddDate(0, 0, wholeDaysPart).Add(durationPart).Truncate(time.Second) + date = date.AddDate(0, 0, wholeDaysPart).Add(durationPart) + if date.Nanosecond()/1e6 > 500 { + return date.Round(time.Second) + } + return date.Truncate(time.Second) } // ExcelDateToTime converts a float-based excel date representation to a time.Time. diff --git a/excelize.go b/excelize.go index 5a47f203b7..a2b61a779f 100644 --- a/excelize.go +++ b/excelize.go @@ -161,7 +161,7 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) { return nil, err } f := newFile() - f.options = parseOptions(opts...) + f.options = getOptions(opts...) if err = f.checkOpenReaderOptions(); err != nil { return nil, err } @@ -198,9 +198,9 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) { return f, err } -// parseOptions provides a function to parse the optional settings for open +// getOptions provides a function to parse the optional settings for open // and reading spreadsheet. -func parseOptions(opts ...Options) *Options { +func getOptions(opts ...Options) *Options { options := &Options{} for _, opt := range opts { options = &opt diff --git a/numfmt_test.go b/numfmt_test.go index f45307d517..5540fb1de5 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -49,9 +49,9 @@ func TestNumFmt(t *testing.T) { {"43543.086539351854", "AM/PM hh:mm:ss a/p", "AM 02:04:37 a"}, {"43528", "YYYY", "2019"}, {"43528", "", "43528"}, - {"43528.2123", "YYYY-MM-DD hh:mm:ss", "2019-03-04 05:05:42"}, - {"43528.2123", "YYYY-MM-DD hh:mm:ss;YYYY-MM-DD hh:mm:ss", "2019-03-04 05:05:42"}, - {"43528.2123", "M/D/YYYY h:m:s", "3/4/2019 5:5:42"}, + {"43528.2123", "YYYY-MM-DD hh:mm:ss", "2019-03-04 05:05:43"}, + {"43528.2123", "YYYY-MM-DD hh:mm:ss;YYYY-MM-DD hh:mm:ss", "2019-03-04 05:05:43"}, + {"43528.2123", "M/D/YYYY h:m:s", "3/4/2019 5:5:43"}, {"43528.003958333335", "m/d/yyyy h:m:s", "3/4/2019 0:5:42"}, {"43528.003958333335", "M/D/YYYY h:mm:s", "3/4/2019 0:05:42"}, {"0.64583333333333337", "h:mm:ss am/pm", "3:30:00 pm"}, diff --git a/rows.go b/rows.go index 4470d2e12e..6b44d8a2f0 100644 --- a/rows.go +++ b/rows.go @@ -138,7 +138,7 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) { } var rowIterator rowXMLIterator var token xml.Token - rows.rawCellValue = parseOptions(opts...).RawCellValue + rows.rawCellValue = getOptions(opts...).RawCellValue if rows.sst, rowIterator.err = rows.f.sharedStringsReader(); rowIterator.err != nil { return rowIterator.cells, rowIterator.err } diff --git a/rows_test.go b/rows_test.go index 20b7a8937b..95e59d9932 100644 --- a/rows_test.go +++ b/rows_test.go @@ -1052,6 +1052,8 @@ func TestNumberFormats(t *testing.T) { assert.NoError(t, err) numFmt10, err := f.NewStyle(&Style{NumFmt: 10}) assert.NoError(t, err) + numFmt21, err := f.NewStyle(&Style{NumFmt: 21}) + assert.NoError(t, err) numFmt37, err := f.NewStyle(&Style{NumFmt: 37}) assert.NoError(t, err) numFmt38, err := f.NewStyle(&Style{NumFmt: 38}) @@ -1093,6 +1095,9 @@ func TestNumberFormats(t *testing.T) { {"A30", numFmt40, -8.8888666665555493e+19, "(88,888,666,665,555,500,000.00)"}, {"A31", numFmt40, 8.8888666665555487, "8.89 "}, {"A32", numFmt40, -8.8888666665555487, "(8.89)"}, + {"A33", numFmt21, 44729.999988368058, "23:59:59"}, + {"A34", numFmt21, 44944.375005787035, "09:00:00"}, + {"A35", numFmt21, 44944.375005798611, "09:00:01"}, } { cell, styleID, value, expected := cases[0].(string), cases[1].(int), cases[2], cases[3].(string) assert.NoError(t, f.SetCellStyle("Sheet1", cell, cell, styleID)) From 1ab7a99bf03cf77f35dd8d14f40d476c7d2d8178 Mon Sep 17 00:00:00 2001 From: Nathan Davies Date: Wed, 25 Jan 2023 04:22:28 +0000 Subject: [PATCH 149/213] This fixes #1455, pre generate strings months name for number format (#1456) - Reducing string concatenation and string conversion between rune string data types Co-authored-by: Nathan Davies --- numfmt.go | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/numfmt.go b/numfmt.go index 9b92140987..e20763f811 100644 --- a/numfmt.go +++ b/numfmt.go @@ -244,8 +244,12 @@ var ( monthNamesWelsh = []string{"Ionawr", "Chwefror", "Mawrth", "Ebrill", "Mai", "Mehefin", "Gorffennaf", "Awst", "Medi", "Hydref", "Tachwedd", "Rhagfyr"} // monthNamesWolof list the month names in the Wolof. monthNamesWolof = []string{"Samwiye", "Fewriye", "Maars", "Awril", "Me", "Suwe", "Sullet", "Ut", "Septàmbar", "Oktoobar", "Noowàmbar", "Desàmbar"} + // monthNamesWolofAbbr list the month name abbreviations in Wolof, this prevents string concatenation + monthNamesWolofAbbr = []string{"Sam.", "Few.", "Maa", "Awr.", "Me", "Suw", "Sul.", "Ut", "Sept.", "Okt.", "Now.", "Des."} // monthNamesXhosa list the month names in the Xhosa. - monthNamesXhosa = []string{"Januwari", "Febuwari", "Matshi", "Aprili", "Meyi", "Juni", "Julayi", "Agasti", "Septemba", "Oktobha", "Novemba", "Disemba"} + monthNamesXhosa = []string{"uJanuwari", "uFebuwari", "uMatshi", "uAprili", "uMeyi", "uJuni", "uJulayi", "uAgasti", "uSeptemba", "uOktobha", "uNovemba", "uDisemba"} + // monthNamesXhosaAbbr list the mont abbreviations in the Xhosa, this prevents string concatenation + monthNamesXhosaAbbr = []string{"uJan.", "uFeb.", "uMat.", "uEpr.", "uMey.", "uJun.", "uJul.", "uAg.", "uSep.", "uOkt.", "uNov.", "uDis."} // monthNamesYi list the month names in the Yi. monthNamesYi = []string{"\ua2cd", "\ua44d", "\ua315", "\ua1d6", "\ua26c", "\ua0d8", "\ua3c3", "\ua246", "\ua22c", "\ua2b0", "\ua2b0\ua2aa", "\ua2b0\ua44b"} // monthNamesZulu list the month names in the Zulu. @@ -594,11 +598,11 @@ func localMonthsNameWelsh(t time.Time, abbr int) string { if abbr == 3 { switch int(t.Month()) { case 2, 7: - return string([]rune(monthNamesWelsh[int(t.Month())-1])[:5]) + return monthNamesWelsh[int(t.Month())-1][:5] case 8, 9, 11, 12: - return string([]rune(monthNamesWelsh[int(t.Month())-1])[:4]) + return monthNamesWelsh[int(t.Month())-1][:4] default: - return string([]rune(monthNamesWelsh[int(t.Month())-1])[:3]) + return monthNamesWelsh[int(t.Month())-1][:3] } } if abbr == 4 { @@ -621,39 +625,21 @@ func localMonthsNameVietnamese(t time.Time, abbr int) string { // localMonthsNameWolof returns the Wolof name of the month. func localMonthsNameWolof(t time.Time, abbr int) string { if abbr == 3 { - switch int(t.Month()) { - case 3, 6: - return string([]rune(monthNamesWolof[int(t.Month())-1])[:3]) - case 5, 8: - return string([]rune(monthNamesWolof[int(t.Month())-1])[:2]) - case 9: - return string([]rune(monthNamesWolof[int(t.Month())-1])[:4]) + "." - case 11: - return "Now." - default: - return string([]rune(monthNamesWolof[int(t.Month())-1])[:3]) + "." - } + return monthNamesWolofAbbr[int(t.Month())-1] } if abbr == 4 { return monthNamesWolof[int(t.Month())-1] } - return string([]rune(monthNamesWolof[int(t.Month())-1])[:1]) + return monthNamesWolof[int(t.Month())-1][:1] } // localMonthsNameXhosa returns the Xhosa name of the month. func localMonthsNameXhosa(t time.Time, abbr int) string { if abbr == 3 { - switch int(t.Month()) { - case 4: - return "uEpr." - case 8: - return "u" + string([]rune(monthNamesXhosa[int(t.Month())-1])[:2]) + "." - default: - return "u" + string([]rune(monthNamesXhosa[int(t.Month())-1])[:3]) + "." - } + return monthNamesXhosaAbbr[int(t.Month())-1] } if abbr == 4 { - return "u" + monthNamesXhosa[int(t.Month())-1] + return monthNamesXhosa[int(t.Month())-1] } return "u" } From be36b09c8a1713bfc5da4ace6c96507133f781bc Mon Sep 17 00:00:00 2001 From: Nathan Davies Date: Thu, 26 Jan 2023 02:35:13 +0000 Subject: [PATCH 150/213] This fixes #1457, reduce string concatenation when applying number format (#1459) Co-authored-by: Nathan Davies --- numfmt.go | 144 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 87 insertions(+), 57 deletions(-) diff --git a/numfmt.go b/numfmt.go index e20763f811..dcad4db20e 100644 --- a/numfmt.go +++ b/numfmt.go @@ -192,22 +192,46 @@ var ( } // monthNamesAfrikaans list the month names in the Afrikaans. monthNamesAfrikaans = []string{"Januarie", "Februarie", "Maart", "April", "Mei", "Junie", "Julie", "Augustus", "September", "Oktober", "November", "Desember"} + // monthNamesAfrikaansAbbr lists the month name abbreviations in Afrikaans + monthNamesAfrikaansAbbr = []string{"Jan.", "Feb.", "Maa.", "Apr.", "Mei", "Jun.", "Jul.", "Aug.", "Sep.", "Okt.", "Nov.", "Des."} // monthNamesChinese list the month names in the Chinese. monthNamesChinese = []string{"一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"} + // monthNamesChineseAbbr1 list the month number and character abbreviation in Chinese + monthNamesChineseAbbrPlus = []string{"0月", "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月"} + // monthNamesChinesePlus list the month names in Chinese plus the character 月 + monthNamesChinesePlus = []string{"一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"} + // monthNamesKoreanAbbrPlus lists out the month number plus 월 for the Korean language + monthNamesKoreanAbbrPlus = []string{"0월", "1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월"} + // monthNamesTradMongolian lists the month number for use with traditional Mongolian + monthNamesTradMongolian = []string{"M01", "M02", "M03", "M04", "M05", "M06", "M07", "M08", "M09", "M10", "M11", "M12"} // monthNamesFrench list the month names in the French. monthNamesFrench = []string{"janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"} + // monthNamesFrenchAbbr lists the month name abbreviations in French + monthNamesFrenchAbbr = []string{"janv.", "févr.", "mars", "avri.", "mai", "juin", "juil.", "août", "sept.", "octo.", "nove.", "déce."} // monthNamesGerman list the month names in the German. monthNamesGerman = []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"} + // monthNamesGermanAbbr list the month abbreviations in German + monthNamesGermanAbbr = []string{"Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"} // monthNamesAustria list the month names in the Austria. monthNamesAustria = []string{"Jänner", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"} + // monthNamesAustriaAbbr list the month name abbreviations in Austrian + monthNamesAustriaAbbr = []string{"Jän", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"} // monthNamesIrish list the month names in the Irish. monthNamesIrish = []string{"Eanáir", "Feabhra", "Márta", "Aibreán", "Bealtaine", "Meitheamh", "Iúil", "Lúnasa", "Meán Fómhair", "Deireadh Fómhair", "Samhain", "Nollaig"} + // monthNamesIrishAbbr lists the month abbreviations in Irish + monthNamesIrishAbbr = []string{"Ean", "Feabh", "Márta", "Aib", "Beal", "Meith", "Iúil", "Lún", "MFómh", "DFómh", "Samh", "Noll"} // monthNamesItalian list the month names in the Italian. monthNamesItalian = []string{"gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"} + // monthNamesItalianAbbr list the month name abbreviations in Italian + monthNamesItalianAbbr = []string{"gen", "feb", "mar", "apr", "mag", "giu", "lug", "ago", "set", "ott", "nov", "dic"} // monthNamesRussian list the month names in the Russian. monthNamesRussian = []string{"январь", "февраль", "март", "апрель", "май", "июнь", "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"} + // monthNamesRussianAbbr list the month abbreviations for Russian. + monthNamesRussianAbbr = []string{"янв.", "фев.", "март", "апр.", "май", "июнь", "июль", "авг.", "сен.", "окт.", "ноя.", "дек."} // monthNamesSpanish list the month names in the Spanish. monthNamesSpanish = []string{"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"} + // monthNamesSpanishAbbr list the month abbreviations in Spanish + monthNamesSpanishAbbr = []string{"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"} // monthNamesThai list the month names in the Thai. monthNamesThai = []string{ "\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21", @@ -238,22 +262,51 @@ var ( "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54\u0f0b", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b", } + // monthNamesTibetanAbbr lists the month name abbreviations in Tibetan + monthNamesTibetanAbbr = []string{ + "\u0f5f\u0fb3\u0f0b\u0f21", + "\u0f5f\u0fb3\u0f0b\u0f22", + "\u0f5f\u0fb3\u0f0b\u0f23", + "\u0f5f\u0fb3\u0f0b\u0f24", + "\u0f5f\u0fb3\u0f0b\u0f25", + "\u0f5f\u0fb3\u0f0b\u0f26", + "\u0f5f\u0fb3\u0f0b\u0f27", + "\u0f5f\u0fb3\u0f0b\u0f28", + "\u0f5f\u0fb3\u0f0b\u0f29", + "\u0f5f\u0fb3\u0f0b\u0f21\u0f20", + "\u0f5f\u0fb3\u0f0b\u0f21\u0f21", + "\u0f5f\u0fb3\u0f0b\u0f21\u0f22", + } // monthNamesTurkish list the month names in the Turkish. monthNamesTurkish = []string{"Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık"} + // monthNamesTurkishAbbr lists the month name abbreviations in Turkish, this prevents string concatenation + monthNamesTurkishAbbr = []string{"Oca", "Şub", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Eki", "Kas", "Ara"} + // monthNamesVietnamese list the month name used for Vietnamese + monthNamesVietnamese = []string{"Tháng 1", "Tháng 2", "Tháng 3", "Tháng 4", "Tháng 5", "Tháng 6", "Tháng 7", "Tháng 8", "Tháng 9", "Tháng 10", "Tháng 11", "Tháng 12"} + // monthNamesVietnameseAbbr3 list the mid-form abbreviation for Vietnamese months + monthNamesVietnameseAbbr3 = []string{"Thg 1", "Thg 2", "Thg 3", "Thg 4", "Thg 5", "Thg 6", "Thg 7", "Thg 8", "Thg 9", "Thg 10", "Thg 11", "Thg 12"} + // monthNamesVietnameseAbbr5 list the short-form abbreviation for Vietnamese months + monthNamesVietnameseAbbr5 = []string{"T 1", "T 2", "T 3", "T 4", "T 5", "T 6", "T 7", "T 8", "T 9", "T 10", "T 11", "T 12"} // monthNamesWelsh list the month names in the Welsh. monthNamesWelsh = []string{"Ionawr", "Chwefror", "Mawrth", "Ebrill", "Mai", "Mehefin", "Gorffennaf", "Awst", "Medi", "Hydref", "Tachwedd", "Rhagfyr"} + // monthNamesWelshAbbr lists the month name abbreviations in Welsh, this prevents string concatenation + monthNamesWelshAbbr = []string{"Ion", "Chwef", "Maw", "Ebr", "Mai", "Meh", "Gorff", "Awst", "Medi", "Hyd", "Tach", "Rhag"} // monthNamesWolof list the month names in the Wolof. monthNamesWolof = []string{"Samwiye", "Fewriye", "Maars", "Awril", "Me", "Suwe", "Sullet", "Ut", "Septàmbar", "Oktoobar", "Noowàmbar", "Desàmbar"} // monthNamesWolofAbbr list the month name abbreviations in Wolof, this prevents string concatenation monthNamesWolofAbbr = []string{"Sam.", "Few.", "Maa", "Awr.", "Me", "Suw", "Sul.", "Ut", "Sept.", "Okt.", "Now.", "Des."} // monthNamesXhosa list the month names in the Xhosa. monthNamesXhosa = []string{"uJanuwari", "uFebuwari", "uMatshi", "uAprili", "uMeyi", "uJuni", "uJulayi", "uAgasti", "uSeptemba", "uOktobha", "uNovemba", "uDisemba"} - // monthNamesXhosaAbbr list the mont abbreviations in the Xhosa, this prevents string concatenation + // monthNamesXhosaAbbr list the month abbreviations in the Xhosa, this prevents string concatenation monthNamesXhosaAbbr = []string{"uJan.", "uFeb.", "uMat.", "uEpr.", "uMey.", "uJun.", "uJul.", "uAg.", "uSep.", "uOkt.", "uNov.", "uDis."} // monthNamesYi list the month names in the Yi. monthNamesYi = []string{"\ua2cd", "\ua44d", "\ua315", "\ua1d6", "\ua26c", "\ua0d8", "\ua3c3", "\ua246", "\ua22c", "\ua2b0", "\ua2b0\ua2aa", "\ua2b0\ua44b"} + // monthNamesYiSuffix lists the month names in Yi with the "\ua1aa" suffix + monthNamesYiSuffix = []string{"\ua2cd\ua1aa", "\ua44d\ua1aa", "\ua315\ua1aa", "\ua1d6\ua1aa", "\ua26c\ua1aa", "\ua0d8\ua1aa", "\ua3c3\ua1aa", "\ua246\ua1aa", "\ua22c\ua1aa", "\ua2b0\ua1aa", "\ua2b0\ua2aa\ua1aa", "\ua2b0\ua44b\ua1aa"} // monthNamesZulu list the month names in the Zulu. monthNamesZulu = []string{"Januwari", "Febhuwari", "Mashi", "Ephreli", "Meyi", "Juni", "Julayi", "Agasti", "Septemba", "Okthoba", "Novemba", "Disemba"} + // monthNamesZuluAbbr list teh month name abbreviations in Zulu + monthNamesZuluAbbr = []string{"Jan", "Feb", "Mas", "Eph", "Mey", "Jun", "Jul", "Agas", "Sep", "Okt", "Nov", "Dis"} // apFmtAfrikaans defined the AM/PM name in the Afrikaans. apFmtAfrikaans = "vm./nm." // apFmtCuba defined the AM/PM name in the Cuba. @@ -399,27 +452,23 @@ func localMonthsNameEnglish(t time.Time, abbr int) string { // localMonthsNameAfrikaans returns the Afrikaans name of the month. func localMonthsNameAfrikaans(t time.Time, abbr int) string { if abbr == 3 { - month := monthNamesAfrikaans[int(t.Month())-1] - if len([]rune(month)) <= 3 { - return month - } - return string([]rune(month)[:3]) + "." + return monthNamesAfrikaansAbbr[int(t.Month())-1] } if abbr == 4 { return monthNamesAfrikaans[int(t.Month())-1] } - return monthNamesAfrikaans[int(t.Month())-1][:1] + return monthNamesAfrikaansAbbr[int(t.Month())-1][:1] } // localMonthsNameAustria returns the Austria name of the month. func localMonthsNameAustria(t time.Time, abbr int) string { if abbr == 3 { - return string([]rune(monthNamesAustria[int(t.Month())-1])[:3]) + return monthNamesAustriaAbbr[int(t.Month())-1] } if abbr == 4 { return monthNamesAustria[int(t.Month())-1] } - return monthNamesAustria[int(t.Month())-1][:1] + return monthNamesAustriaAbbr[int(t.Month())-1][:1] } // localMonthsNameBangla returns the German name of the month. @@ -435,65 +484,56 @@ func localMonthsNameFrench(t time.Time, abbr int) string { if abbr == 3 { month := monthNamesFrench[int(t.Month())-1] if len([]rune(month)) <= 4 { - return month + return monthNamesFrench[int(t.Month())-1] } - return string([]rune(month)[:4]) + "." + return monthNamesFrenchAbbr[int(t.Month())-1] } if abbr == 4 { return monthNamesFrench[int(t.Month())-1] } - return monthNamesFrench[int(t.Month())-1][:1] + return monthNamesFrenchAbbr[int(t.Month())-1][:1] } // localMonthsNameIrish returns the Irish name of the month. func localMonthsNameIrish(t time.Time, abbr int) string { if abbr == 3 { - switch int(t.Month()) { - case 1, 4, 8: - return string([]rune(monthNamesIrish[int(t.Month())-1])[:3]) - case 2, 3, 6: - return string([]rune(monthNamesIrish[int(t.Month())-1])[:5]) - case 9, 10: - return string([]rune(monthNamesIrish[int(t.Month())-1])[:1]) + "Fómh" - default: - return string([]rune(monthNamesIrish[int(t.Month())-1])[:4]) - } + return monthNamesIrishAbbr[int(t.Month()-1)] } if abbr == 4 { return monthNamesIrish[int(t.Month())-1] } - return string([]rune(monthNamesIrish[int(t.Month())-1])[:1]) + return monthNamesIrishAbbr[int(t.Month())-1][:1] } // localMonthsNameItalian returns the Italian name of the month. func localMonthsNameItalian(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesItalian[int(t.Month())-1][:3] + return monthNamesItalianAbbr[int(t.Month())-1] } if abbr == 4 { return monthNamesItalian[int(t.Month())-1] } - return monthNamesItalian[int(t.Month())-1][:1] + return monthNamesItalianAbbr[int(t.Month())-1][:1] } // localMonthsNameGerman returns the German name of the month. func localMonthsNameGerman(t time.Time, abbr int) string { if abbr == 3 { - return string([]rune(monthNamesGerman[int(t.Month())-1])[:3]) + return monthNamesGermanAbbr[int(t.Month())-1] } if abbr == 4 { return monthNamesGerman[int(t.Month())-1] } - return string([]rune(monthNamesGerman[int(t.Month())-1])[:1]) + return monthNamesGermanAbbr[int(t.Month())-1][:1] } // localMonthsNameChinese1 returns the Chinese name of the month. func localMonthsNameChinese1(t time.Time, abbr int) string { if abbr == 3 { - return strconv.Itoa(int(t.Month())) + "月" + return monthNamesChineseAbbrPlus[int(t.Month())] } if abbr == 4 { - return monthNamesChinese[int(t.Month())-1] + "月" + return monthNamesChinesePlus[int(t.Month())-1] } return monthNamesChinese[int(t.Month())-1] } @@ -501,7 +541,7 @@ func localMonthsNameChinese1(t time.Time, abbr int) string { // localMonthsNameChinese2 returns the Chinese name of the month. func localMonthsNameChinese2(t time.Time, abbr int) string { if abbr == 3 || abbr == 4 { - return monthNamesChinese[int(t.Month())-1] + "月" + return monthNamesChinesePlus[int(t.Month())-1] } return monthNamesChinese[int(t.Month())-1] } @@ -509,7 +549,7 @@ func localMonthsNameChinese2(t time.Time, abbr int) string { // localMonthsNameChinese3 returns the Chinese name of the month. func localMonthsNameChinese3(t time.Time, abbr int) string { if abbr == 3 || abbr == 4 { - return strconv.Itoa(int(t.Month())) + "月" + return monthNamesChineseAbbrPlus[int(t.Month())] } return strconv.Itoa(int(t.Month())) } @@ -517,7 +557,7 @@ func localMonthsNameChinese3(t time.Time, abbr int) string { // localMonthsNameKorean returns the Korean name of the month. func localMonthsNameKorean(t time.Time, abbr int) string { if abbr == 3 || abbr == 4 { - return strconv.Itoa(int(t.Month())) + "월" + return monthNamesKoreanAbbrPlus[int(t.Month())] } return strconv.Itoa(int(t.Month())) } @@ -527,7 +567,7 @@ func localMonthsNameTraditionalMongolian(t time.Time, abbr int) string { if abbr == 5 { return "M" } - return fmt.Sprintf("M%02d", int(t.Month())) + return monthNamesTradMongolian[int(t.Month()-1)] } // localMonthsNameRussian returns the Russian name of the month. @@ -537,7 +577,7 @@ func localMonthsNameRussian(t time.Time, abbr int) string { if len([]rune(month)) <= 4 { return month } - return string([]rune(month)[:3]) + "." + return monthNamesRussianAbbr[int(t.Month())-1] } if abbr == 4 { return monthNamesRussian[int(t.Month())-1] @@ -548,12 +588,12 @@ func localMonthsNameRussian(t time.Time, abbr int) string { // localMonthsNameSpanish returns the Spanish name of the month. func localMonthsNameSpanish(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSpanish[int(t.Month())-1][:3] + return monthNamesSpanishAbbr[int(t.Month())-1] } if abbr == 4 { return monthNamesSpanish[int(t.Month())-1] } - return monthNamesSpanish[int(t.Month())-1][:1] + return monthNamesSpanishAbbr[int(t.Month())-1][:1] } // localMonthsNameThai returns the Thai name of the month. @@ -571,7 +611,7 @@ func localMonthsNameThai(t time.Time, abbr int) string { // localMonthsNameTibetan returns the Tibetan name of the month. func localMonthsNameTibetan(t time.Time, abbr int) string { if abbr == 3 { - return "\u0f5f\u0fb3\u0f0b" + []string{"\u0f21", "\u0f22", "\u0f23", "\u0f24", "\u0f25", "\u0f26", "\u0f27", "\u0f28", "\u0f29", "\u0f21\u0f20", "\u0f21\u0f21", "\u0f21\u0f22"}[int(t.Month())-1] + return monthNamesTibetanAbbr[int(t.Month())-1] } if abbr == 5 { if t.Month() == 10 { @@ -585,41 +625,34 @@ func localMonthsNameTibetan(t time.Time, abbr int) string { // localMonthsNameTurkish returns the Turkish name of the month. func localMonthsNameTurkish(t time.Time, abbr int) string { if abbr == 3 { - return string([]rune(monthNamesTurkish[int(t.Month())-1])[:3]) + return monthNamesTurkishAbbr[int(t.Month())-1] } if abbr == 4 { return monthNamesTurkish[int(t.Month())-1] } - return string([]rune(monthNamesTurkish[int(t.Month())-1])[:1]) + return string([]rune(monthNamesTurkishAbbr[int(t.Month())-1])[:1]) } // localMonthsNameWelsh returns the Welsh name of the month. func localMonthsNameWelsh(t time.Time, abbr int) string { if abbr == 3 { - switch int(t.Month()) { - case 2, 7: - return monthNamesWelsh[int(t.Month())-1][:5] - case 8, 9, 11, 12: - return monthNamesWelsh[int(t.Month())-1][:4] - default: - return monthNamesWelsh[int(t.Month())-1][:3] - } + return monthNamesWelshAbbr[int(t.Month())-1] } if abbr == 4 { return monthNamesWelsh[int(t.Month())-1] } - return string([]rune(monthNamesWelsh[int(t.Month())-1])[:1]) + return monthNamesWelshAbbr[int(t.Month())-1][:1] } // localMonthsNameVietnamese returns the Vietnamese name of the month. func localMonthsNameVietnamese(t time.Time, abbr int) string { if abbr == 3 { - return "Thg " + strconv.Itoa(int(t.Month())) + return monthNamesVietnameseAbbr3[int(t.Month()-1)] } if abbr == 5 { - return "T " + strconv.Itoa(int(t.Month())) + return monthNamesVietnameseAbbr5[int(t.Month()-1)] } - return "Tháng " + strconv.Itoa(int(t.Month())) + return monthNamesVietnamese[int(t.Month()-1)] } // localMonthsNameWolof returns the Wolof name of the month. @@ -647,7 +680,7 @@ func localMonthsNameXhosa(t time.Time, abbr int) string { // localMonthsNameYi returns the Yi name of the month. func localMonthsNameYi(t time.Time, abbr int) string { if abbr == 3 || abbr == 4 { - return string(monthNamesYi[int(t.Month())-1]) + "\ua1aa" + return monthNamesYiSuffix[int(t.Month()-1)] } return string([]rune(monthNamesYi[int(t.Month())-1])[:1]) } @@ -655,15 +688,12 @@ func localMonthsNameYi(t time.Time, abbr int) string { // localMonthsNameZulu returns the Zulu name of the month. func localMonthsNameZulu(t time.Time, abbr int) string { if abbr == 3 { - if int(t.Month()) == 8 { - return string([]rune(monthNamesZulu[int(t.Month())-1])[:4]) - } - return string([]rune(monthNamesZulu[int(t.Month())-1])[:3]) + return monthNamesZuluAbbr[int(t.Month()-1)] } if abbr == 4 { return monthNamesZulu[int(t.Month())-1] } - return string([]rune(monthNamesZulu[int(t.Month())-1])[:1]) + return monthNamesZuluAbbr[int(t.Month())-1][:1] } // localMonthName return months name by supported language ID. From 85e0b6c56eb14c7eabf4332a778bf654f83295ca Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 1 Feb 2023 00:11:08 +0800 Subject: [PATCH 151/213] Support to create of 17 kinds of fill variants styles - Update the unit tests - Update the `SetHeaderFooter` function parameters name - Update document for the `SetDocProps` and `SetCellHyperLink` functions --- cell.go | 2 +- docProps.go | 32 ++++++++++++++++------------ sheet.go | 26 +++++++++++------------ styles.go | 57 ++++++++++++++++++++++++-------------------------- styles_test.go | 7 +++++++ 5 files changed, 67 insertions(+), 57 deletions(-) diff --git a/cell.go b/cell.go index 3005222cfa..38366e4a5e 100644 --- a/cell.go +++ b/cell.go @@ -835,7 +835,7 @@ type HyperlinkOpts struct { // // display, tooltip := "https://github.com/xuri/excelize", "Excelize on GitHub" // if err := f.SetCellHyperLink("Sheet1", "A3", -// "https://github.com/xuri/excelize", "External", excelize.HyperlinkOpts{ +// display, "External", excelize.HyperlinkOpts{ // Display: &display, // Tooltip: &tooltip, // }); err != nil { diff --git a/docProps.go b/docProps.go index 3dca7bd2ac..3d81545497 100644 --- a/docProps.go +++ b/docProps.go @@ -120,39 +120,45 @@ func (f *File) GetAppProps() (ret *AppProperties, err error) { // properties that can be set are: // // Property | Description -// ----------------+----------------------------------------------------------------------------- +// ----------------+----------------------------------------------------------- // Title | The name given to the resource. // | // Subject | The topic of the content of the resource. // | -// Creator | An entity primarily responsible for making the content of the resource. +// Creator | An entity primarily responsible for making the content of +// | the resource. // | -// Keywords | A delimited set of keywords to support searching and indexing. This is -// | typically a list of terms that are not available elsewhere in the properties. +// Keywords | A delimited set of keywords to support searching and +// | indexing. This is typically a list of terms that are not +// | available elsewhere in the properties. // | // Description | An explanation of the content of the resource. // | -// LastModifiedBy | The user who performed the last modification. The identification is -// | environment-specific. +// LastModifiedBy | The user who performed the last modification. The +// | identification is environment-specific. // | // Language | The language of the intellectual content of the resource. // | -// Identifier | An unambiguous reference to the resource within a given context. +// Identifier | An unambiguous reference to the resource within a given +// | context. // | // Revision | The topic of the content of the resource. // | -// ContentStatus | The status of the content. For example: Values might include "Draft", -// | "Reviewed" and "Final" +// ContentStatus | The status of the content. For example: Values might +// | include "Draft", "Reviewed" and "Final" // | // Category | A categorization of the content of this package. // | -// Version | The version number. This value is set by the user or by the application. +// Version | The version number. This value is set by the user or by +// | the application. // | -// Modified | The created time of the content of the resource which -// | represent in ISO 8601 UTC format, for example "2019-06-04T22:00:10Z". +// Created | The created time of the content of the resource which +// | represent in ISO 8601 UTC format, for example +// | "2019-06-04T22:00:10Z". // | // Modified | The modified time of the content of the resource which -// | represent in ISO 8601 UTC format, for example "2019-06-04T22:00:10Z". +// | represent in ISO 8601 UTC format, for example +// | "2019-06-04T22:00:10Z". // | // // For example: diff --git a/sheet.go b/sheet.go index 9d1f5f9bc4..3ea489a50e 100644 --- a/sheet.go +++ b/sheet.go @@ -1183,17 +1183,17 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { // that same page // // - No footer on the first page -func (f *File) SetHeaderFooter(sheet string, settings *HeaderFooterOptions) error { +func (f *File) SetHeaderFooter(sheet string, opts *HeaderFooterOptions) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } - if settings == nil { + if opts == nil { ws.HeaderFooter = nil return err } - v := reflect.ValueOf(*settings) + v := reflect.ValueOf(*opts) // Check 6 string type fields: OddHeader, OddFooter, EvenHeader, EvenFooter, // FirstFooter, FirstHeader for i := 4; i < v.NumField()-1; i++ { @@ -1202,16 +1202,16 @@ func (f *File) SetHeaderFooter(sheet string, settings *HeaderFooterOptions) erro } } ws.HeaderFooter = &xlsxHeaderFooter{ - AlignWithMargins: settings.AlignWithMargins, - DifferentFirst: settings.DifferentFirst, - DifferentOddEven: settings.DifferentOddEven, - ScaleWithDoc: settings.ScaleWithDoc, - OddHeader: settings.OddHeader, - OddFooter: settings.OddFooter, - EvenHeader: settings.EvenHeader, - EvenFooter: settings.EvenFooter, - FirstFooter: settings.FirstFooter, - FirstHeader: settings.FirstHeader, + AlignWithMargins: opts.AlignWithMargins, + DifferentFirst: opts.DifferentFirst, + DifferentOddEven: opts.DifferentOddEven, + ScaleWithDoc: opts.ScaleWithDoc, + OddHeader: opts.OddHeader, + OddFooter: opts.OddFooter, + EvenHeader: opts.EvenHeader, + EvenFooter: opts.EvenFooter, + FirstFooter: opts.FirstFooter, + FirstHeader: opts.FirstHeader, } return err } diff --git a/styles.go b/styles.go index e5d933cd6b..af32324500 100644 --- a/styles.go +++ b/styles.go @@ -1145,9 +1145,9 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // // Index | Style | Index | Style // -------+-----------------+-------+----------------- -// 0 | Horizontal | 3 | Diagonal down -// 1 | Vertical | 4 | From corner -// 2 | Diagonal Up | 5 | From center +// 0-2 | Horizontal | 9-11 | Diagonal down +// 3-5 | Vertical | 12-15 | From corner +// 6-8 | Diagonal Up | 16 | From center // // The following table shows the pattern styles used in 'Fill.Pattern' supported // by excelize index number: @@ -2459,41 +2459,38 @@ func newFills(style *Style, fg bool) *xlsxFill { "gray125", "gray0625", } - - variants := []float64{ - 90, - 0, - 45, - 135, + variants := []xlsxGradientFill{ + {Degree: 90, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 270, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 90, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 180, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}}, + {Degree: 45, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 255, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 45, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}}, + {Degree: 135, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 315, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 135, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path"}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Left: 1, Right: 1}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 1, Top: 1}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 1, Left: 1, Right: 1, Top: 1}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 0.5, Left: 0.5, Right: 0.5, Top: 0.5}, } var fill xlsxFill switch style.Fill.Type { case "gradient": - if len(style.Fill.Color) != 2 { + if len(style.Fill.Color) != 2 || style.Fill.Shading < 0 || style.Fill.Shading > 16 { break } - var gradient xlsxGradientFill - switch style.Fill.Shading { - case 0, 1, 2, 3: - gradient.Degree = variants[style.Fill.Shading] - case 4: - gradient.Type = "path" - case 5: - gradient.Type = "path" - gradient.Bottom = 0.5 - gradient.Left = 0.5 - gradient.Right = 0.5 - gradient.Top = 0.5 - } - var stops []*xlsxGradientFillStop - for index, color := range style.Fill.Color { - var stop xlsxGradientFillStop - stop.Position = float64(index) - stop.Color.RGB = getPaletteColor(color) - stops = append(stops, &stop) + gradient := variants[style.Fill.Shading] + gradient.Stop[0].Color.RGB = getPaletteColor(style.Fill.Color[0]) + gradient.Stop[1].Color.RGB = getPaletteColor(style.Fill.Color[1]) + if len(gradient.Stop) == 3 { + gradient.Stop[2].Color.RGB = getPaletteColor(style.Fill.Color[0]) } - gradient.Stop = stops fill.GradientFill = &gradient case "pattern": if style.Fill.Pattern > 18 || style.Fill.Pattern < 0 { diff --git a/styles_test.go b/styles_test.go index 53cd7cdd78..79fb7b3dd1 100644 --- a/styles_test.go +++ b/styles_test.go @@ -223,6 +223,13 @@ func TestUnsetConditionalFormat(t *testing.T) { func TestNewStyle(t *testing.T) { f := NewFile() + for i := 0; i < 18; i++ { + _, err := f.NewStyle(&Style{ + Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF", "#4E71BE"}, Shading: i}, + }) + assert.NoError(t, err) + } + f = NewFile() styleID, err := f.NewStyle(&Style{Font: &Font{Bold: true, Italic: true, Family: "Times New Roman", Size: 36, Color: "#777777"}}) assert.NoError(t, err) styles, err := f.stylesReader() From 12645e711656844b72b5b02fc6f97befc2d3053d Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 2 Feb 2023 22:02:32 +0800 Subject: [PATCH 152/213] This fixes #1461, supports 0 row height and column width - Increase max cell styles to 65430 - Add new exported error variable `ErrCellStyles` - Update unit tests, support test under Go 1.20.x --- .github/workflows/go.yml | 2 +- col.go | 14 ++++++------ errors.go | 2 ++ lib_test.go | 6 ++++-- rows.go | 12 +++++------ sheet.go | 4 ++-- styles.go | 11 ++++++---- styles_test.go | 7 ++++++ xmlDrawing.go | 2 +- xmlWorksheet.go | 46 ++++++++++++++++++++-------------------- 10 files changed, 60 insertions(+), 46 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 46f92f5fe4..15c98857cf 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,7 +5,7 @@ jobs: test: strategy: matrix: - go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x] + go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x] os: [ubuntu-latest, macos-latest, windows-latest] targetplatform: [x86, x64] diff --git a/col.go b/col.go index d3852048a7..74f9cddaaf 100644 --- a/col.go +++ b/col.go @@ -300,7 +300,7 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error { colData := xlsxCol{ Min: min, Max: max, - Width: defaultColWidth, // default width + Width: float64Ptr(defaultColWidth), Hidden: !visible, CustomWidth: true, } @@ -449,7 +449,7 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { ws.Cols.Col = flatCols(xlsxCol{ Min: min, Max: max, - Width: defaultColWidth, + Width: float64Ptr(defaultColWidth), Style: styleID, }, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol { fc.BestFit = c.BestFit @@ -493,7 +493,7 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error col := xlsxCol{ Min: min, Max: max, - Width: width, + Width: float64Ptr(width), CustomWidth: true, } if ws.Cols == nil { @@ -639,8 +639,8 @@ func (f *File) getColWidth(sheet string, col int) int { if ws.Cols != nil { var width float64 for _, v := range ws.Cols.Col { - if v.Min <= col && col <= v.Max { - width = v.Width + if v.Min <= col && col <= v.Max && v.Width != nil { + width = *v.Width } } if width != 0 { @@ -691,8 +691,8 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { if ws.Cols != nil { var width float64 for _, v := range ws.Cols.Col { - if v.Min <= colNum && colNum <= v.Max { - width = v.Width + if v.Min <= colNum && colNum <= v.Max && v.Width != nil { + width = *v.Width } } if width != 0 { diff --git a/errors.go b/errors.go index 471afa62f5..2a627d3de4 100644 --- a/errors.go +++ b/errors.go @@ -230,6 +230,8 @@ var ( // ErrSheetNameLength defined the error message on receiving the sheet // name length exceeds the limit. ErrSheetNameLength = fmt.Errorf("the sheet name length exceeds the %d characters limit", MaxSheetNameLength) + // ErrCellStyles defined the error message on cell styles exceeds the limit. + ErrCellStyles = fmt.Errorf("the cell styles exceeds the %d limit", MaxCellStyles) // ErrUnprotectWorkbook defined the error message on workbook has set no // protection. ErrUnprotectWorkbook = errors.New("workbook has set no protect") diff --git a/lib_test.go b/lib_test.go index ec5fd640db..ab0ccc9ae8 100644 --- a/lib_test.go +++ b/lib_test.go @@ -342,8 +342,10 @@ func TestReadBytes(t *testing.T) { } func TestUnzipToTemp(t *testing.T) { - if strings.HasPrefix(runtime.Version(), "go1.19") { - t.Skip() + for _, v := range []string{"go1.19", "go1.20"} { + if strings.HasPrefix(runtime.Version(), v) { + t.Skip() + } } os.Setenv("TMPDIR", "test") defer os.Unsetenv("TMPDIR") diff --git a/rows.go b/rows.go index 6b44d8a2f0..05257e53e8 100644 --- a/rows.go +++ b/rows.go @@ -363,7 +363,7 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { prepareSheetXML(ws, 0, row) rowIdx := row - 1 - ws.SheetData.Row[rowIdx].Ht = height + ws.SheetData.Row[rowIdx].Ht = float64Ptr(height) ws.SheetData.Row[rowIdx].CustomHeight = true return nil } @@ -376,8 +376,8 @@ func (f *File) getRowHeight(sheet string, row int) int { defer ws.Unlock() for i := range ws.SheetData.Row { v := &ws.SheetData.Row[i] - if v.R == row && v.Ht != 0 { - return int(convertRowHeightToPixels(v.Ht)) + if v.R == row && v.Ht != nil { + return int(convertRowHeightToPixels(*v.Ht)) } } // Optimization for when the row heights haven't changed. @@ -404,8 +404,8 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { return ht, nil // it will be better to use 0, but we take care with BC } for _, v := range ws.SheetData.Row { - if v.R == row && v.Ht != 0 { - return v.Ht, nil + if v.R == row && v.Ht != nil { + return *v.Ht, nil } } // Optimization for when the row heights haven't changed. @@ -784,7 +784,7 @@ func checkRow(ws *xlsxWorksheet) error { // hasAttr determine if row non-default attributes. func (r *xlsxRow) hasAttr() bool { - return r.Spans != "" || r.S != 0 || r.CustomFormat || r.Ht != 0 || + return r.Spans != "" || r.S != 0 || r.CustomFormat || r.Ht != nil || r.Hidden || r.CustomHeight || r.OutlineLevel != 0 || r.Collapsed || r.ThickTop || r.ThickBot || r.Ph } diff --git a/sheet.go b/sheet.go index 3ea489a50e..814989ada9 100644 --- a/sheet.go +++ b/sheet.go @@ -1844,10 +1844,10 @@ func prepareSheetXML(ws *xlsxWorksheet, col int, row int) { defer ws.Unlock() rowCount := len(ws.SheetData.Row) sizeHint := 0 - var ht float64 + var ht *float64 var customHeight bool if ws.SheetFormatPr != nil && ws.SheetFormatPr.CustomHeight { - ht = ws.SheetFormatPr.DefaultRowHeight + ht = float64Ptr(ws.SheetFormatPr.DefaultRowHeight) customHeight = true } if rowCount > 0 { diff --git a/styles.go b/styles.go index af32324500..e2be993677 100644 --- a/styles.go +++ b/styles.go @@ -1193,6 +1193,7 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // // Style // ------------------ +// none // single // double // @@ -2047,8 +2048,7 @@ func (f *File) NewStyle(style *Style) (int, error) { applyAlignment, alignment := fs.Alignment != nil, newAlignment(fs) applyProtection, protection := fs.Protection != nil, newProtection(fs) - cellXfsID = setCellXfs(s, fontID, numFmtID, fillID, borderID, applyAlignment, applyProtection, alignment, protection) - return cellXfsID, nil + return setCellXfs(s, fontID, numFmtID, fillID, borderID, applyAlignment, applyProtection, alignment, protection) } var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ @@ -2620,7 +2620,7 @@ func newBorders(style *Style) *xlsxBorder { // setCellXfs provides a function to set describes all of the formatting for a // cell. -func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, applyAlignment, applyProtection bool, alignment *xlsxAlignment, protection *xlsxProtection) int { +func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, applyAlignment, applyProtection bool, alignment *xlsxAlignment, protection *xlsxProtection) (int, error) { var xf xlsxXf xf.FontID = intPtr(fontID) if fontID != 0 { @@ -2638,6 +2638,9 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a if borderID != 0 { xf.ApplyBorder = boolPtr(true) } + if len(style.CellXfs.Xf) == MaxCellStyles { + return 0, ErrCellStyles + } style.CellXfs.Count = len(style.CellXfs.Xf) + 1 xf.Alignment = alignment if alignment != nil { @@ -2650,7 +2653,7 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a xfID := 0 xf.XfID = &xfID style.CellXfs.Xf = append(style.CellXfs.Xf, xf) - return style.CellXfs.Count - 1 + return style.CellXfs.Count - 1, nil } // GetCellStyle provides a function to get cell style index by given worksheet diff --git a/styles_test.go b/styles_test.go index 79fb7b3dd1..86860fad59 100644 --- a/styles_test.go +++ b/styles_test.go @@ -325,6 +325,13 @@ func TestNewStyle(t *testing.T) { f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) _, err = f.NewStyle(&Style{NumFmt: 165}) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + + // Test create cell styles reach maximum + f = NewFile() + f.Styles.CellXfs.Xf = make([]xlsxXf, MaxCellStyles) + f.Styles.CellXfs.Count = MaxCellStyles + _, err = f.NewStyle(&Style{NumFmt: 0}) + assert.Equal(t, ErrCellStyles, err) } func TestNewConditionalStyle(t *testing.T) { diff --git a/xmlDrawing.go b/xmlDrawing.go index 30b4d0942e..125e5e4869 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -108,7 +108,7 @@ const ( // Excel specifications and limits const ( - MaxCellStyles = 64000 + MaxCellStyles = 65430 MaxColumns = 16384 MaxColumnWidth = 255 MaxFieldLength = 255 diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 4e3a35860e..8b45a34e5c 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -280,16 +280,16 @@ type xlsxCols struct { // xlsxCol directly maps the col (Column Width & Formatting). Defines column // width and column formatting for one or more columns of the worksheet. type xlsxCol struct { - BestFit bool `xml:"bestFit,attr,omitempty"` - Collapsed bool `xml:"collapsed,attr,omitempty"` - CustomWidth bool `xml:"customWidth,attr,omitempty"` - Hidden bool `xml:"hidden,attr,omitempty"` - Max int `xml:"max,attr"` - Min int `xml:"min,attr"` - OutlineLevel uint8 `xml:"outlineLevel,attr,omitempty"` - Phonetic bool `xml:"phonetic,attr,omitempty"` - Style int `xml:"style,attr,omitempty"` - Width float64 `xml:"width,attr,omitempty"` + BestFit bool `xml:"bestFit,attr,omitempty"` + Collapsed bool `xml:"collapsed,attr,omitempty"` + CustomWidth bool `xml:"customWidth,attr,omitempty"` + Hidden bool `xml:"hidden,attr,omitempty"` + Max int `xml:"max,attr"` + Min int `xml:"min,attr"` + OutlineLevel uint8 `xml:"outlineLevel,attr,omitempty"` + Phonetic bool `xml:"phonetic,attr,omitempty"` + Style int `xml:"style,attr,omitempty"` + Width *float64 `xml:"width,attr"` } // xlsxDimension directly maps the dimension element in the namespace @@ -316,19 +316,19 @@ type xlsxSheetData struct { // about an entire row of a worksheet, and contains all cell definitions for a // particular row in the worksheet. type xlsxRow struct { - C []xlsxC `xml:"c"` - R int `xml:"r,attr,omitempty"` - Spans string `xml:"spans,attr,omitempty"` - S int `xml:"s,attr,omitempty"` - CustomFormat bool `xml:"customFormat,attr,omitempty"` - Ht float64 `xml:"ht,attr,omitempty"` - Hidden bool `xml:"hidden,attr,omitempty"` - CustomHeight bool `xml:"customHeight,attr,omitempty"` - OutlineLevel uint8 `xml:"outlineLevel,attr,omitempty"` - Collapsed bool `xml:"collapsed,attr,omitempty"` - ThickTop bool `xml:"thickTop,attr,omitempty"` - ThickBot bool `xml:"thickBot,attr,omitempty"` - Ph bool `xml:"ph,attr,omitempty"` + C []xlsxC `xml:"c"` + R int `xml:"r,attr,omitempty"` + Spans string `xml:"spans,attr,omitempty"` + S int `xml:"s,attr,omitempty"` + CustomFormat bool `xml:"customFormat,attr,omitempty"` + Ht *float64 `xml:"ht,attr"` + Hidden bool `xml:"hidden,attr,omitempty"` + CustomHeight bool `xml:"customHeight,attr,omitempty"` + OutlineLevel uint8 `xml:"outlineLevel,attr,omitempty"` + Collapsed bool `xml:"collapsed,attr,omitempty"` + ThickTop bool `xml:"thickTop,attr,omitempty"` + ThickBot bool `xml:"thickBot,attr,omitempty"` + Ph bool `xml:"ph,attr,omitempty"` } // xlsxSortState directly maps the sortState element. This collection From 1f69f6b24af45bcaa0e9cf1ea6f5426ad3756e87 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 5 Feb 2023 00:21:23 +0800 Subject: [PATCH 153/213] Add support for insert BMP format images - Add support for workbook function groups - Update code and docs for the build-in currency number format - Update unit tests --- picture.go | 9 ++++---- picture_test.go | 2 ++ sheet.go | 6 ++--- sheet_test.go | 5 ++++- styles.go | 50 +++++++++++++++++++++--------------------- test/images/excel.bmp | Bin 0 -> 26678 bytes xmlDrawing.go | 6 ++--- xmlWorkbook.go | 13 ++++++++++- 8 files changed, 54 insertions(+), 37 deletions(-) create mode 100755 test/images/excel.bmp diff --git a/picture.go b/picture.go index 067f1bf79e..f003852e27 100644 --- a/picture.go +++ b/picture.go @@ -51,8 +51,8 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { // AddPicture provides the method to add picture in a sheet by given picture // format set (such as offset, scale, aspect ratio setting and print settings) -// and file path, supported image types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, -// TIF, TIFF, WMF, and WMZ. This function is concurrency safe. For example: +// and file path, supported image types: BMP, EMF, EMZ, GIF, JPEG, JPG, PNG, +// SVG, TIF, TIFF, WMF, and WMZ. This function is concurrency safe. For example: // // package main // @@ -436,8 +436,9 @@ func (f *File) addMedia(file []byte, ext string) string { // type for relationship parts and the Main Document part. func (f *File) setContentTypePartImageExtensions() error { imageTypes := map[string]string{ - "jpeg": "image/", "png": "image/", "gif": "image/", "svg": "image/", "tiff": "image/", - "emf": "image/x-", "wmf": "image/x-", "emz": "image/x-", "wmz": "image/x-", + "bmp": "image/", "jpeg": "image/", "png": "image/", "gif": "image/", + "svg": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-", + "emz": "image/x-", "wmz": "image/x-", } content, err := f.contentTypesReader() if err != nil { diff --git a/picture_test.go b/picture_test.go index eda36ffc13..d6b91c7f7f 100644 --- a/picture_test.go +++ b/picture_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + _ "golang.org/x/image/bmp" _ "golang.org/x/image/tiff" "github.com/stretchr/testify/assert" @@ -64,6 +65,7 @@ func TestAddPicture(t *testing.T) { assert.NoError(t, f.AddPicture("Sheet1", "Q8", filepath.Join("test", "images", "excel.gif"), nil)) assert.NoError(t, f.AddPicture("Sheet1", "Q15", filepath.Join("test", "images", "excel.jpg"), nil)) assert.NoError(t, f.AddPicture("Sheet1", "Q22", filepath.Join("test", "images", "excel.tif"), nil)) + assert.NoError(t, f.AddPicture("Sheet1", "Q28", filepath.Join("test", "images", "excel.bmp"), nil)) // Test write file to given path assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx"))) diff --git a/sheet.go b/sheet.go index 814989ada9..68cbf50ea3 100644 --- a/sheet.go +++ b/sheet.go @@ -500,8 +500,8 @@ func (f *File) getSheetXMLPath(sheet string) (string, bool) { } // SetSheetBackground provides a function to set background picture by given -// worksheet name and file path. Supported image types: EMF, EMZ, GIF, JPEG, -// JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. +// worksheet name and file path. Supported image types: BMP, EMF, EMZ, GIF, +// JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. func (f *File) SetSheetBackground(sheet, picture string) error { var err error // Check picture exists first. @@ -514,7 +514,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { // SetSheetBackgroundFromBytes provides a function to set background picture by // given worksheet name, extension name and image data. Supported image types: -// EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. +// BMP, EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. func (f *File) SetSheetBackgroundFromBytes(sheet, extension string, picture []byte) error { if len(picture) == 0 { return ErrParameterInvalid diff --git a/sheet_test.go b/sheet_test.go index f809fe8f99..6d2e0f6d5c 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -596,7 +596,10 @@ func TestAttrValToFloat(t *testing.T) { func TestSetSheetBackgroundFromBytes(t *testing.T) { f := NewFile() assert.NoError(t, f.SetSheetName("Sheet1", ".svg")) - for i, imageTypes := range []string{".svg", ".emf", ".emz", ".gif", ".jpg", ".png", ".tif", ".wmf", ".wmz"} { + for i, imageTypes := range []string{ + ".svg", ".bmp", ".emf", ".emz", ".gif", + ".jpg", ".png", ".tif", ".wmf", ".wmz", + } { file := fmt.Sprintf("excelize%s", imageTypes) if i > 0 { file = filepath.Join("test", "images", fmt.Sprintf("excel%s", imageTypes)) diff --git a/styles.go b/styles.go index e2be993677..5a77bc7c7b 100644 --- a/styles.go +++ b/styles.go @@ -277,7 +277,7 @@ var langNumFmt = map[string]map[int]string{ // currencyNumFmt defined the currency number format map. var currencyNumFmt = map[int]string{ - 164: `"CN¥",##0.00`, + 164: `"¥"#,##0.00`, 165: "[$$-409]#,##0.00", 166: "[$$-45C]#,##0.00", 167: "[$$-1004]#,##0.00", @@ -1491,8 +1491,8 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // // Index | Symbol // -------+--------------------------------------------------------------- -// 164 | CN¥ -// 165 | $ English (China) +// 164 | ¥ +// 165 | $ English (United States) // 166 | $ Cherokee (United States) // 167 | $ Chinese (Singapore) // 168 | $ Chinese (Taiwan) @@ -1533,28 +1533,28 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // 203 | ₡ Spanish (Costa Rica) // 204 | ₦ Hausa (Nigeria) // 205 | ₦ Igbo (Nigeria) -// 206 | ₦ Yoruba (Nigeria) -// 207 | ₩ Korean (South Korea) -// 208 | ₪ Hebrew (Israel) -// 209 | ₫ Vietnamese (Vietnam) -// 210 | € Basque (Spain) -// 211 | € Breton (France) -// 212 | € Catalan (Spain) -// 213 | € Corsican (France) -// 214 | € Dutch (Belgium) -// 215 | € Dutch (Netherlands) -// 216 | € English (Ireland) -// 217 | € Estonian (Estonia) -// 218 | € Euro (€ 123) -// 219 | € Euro (123 €) -// 220 | € Finnish (Finland) -// 221 | € French (Belgium) -// 222 | € French (France) -// 223 | € French (Luxembourg) -// 224 | € French (Monaco) -// 225 | € French (Réunion) -// 226 | € Galician (Spain) -// 227 | € German (Austria) +// 206 | ₩ Korean (South Korea) +// 207 | ₪ Hebrew (Israel) +// 208 | ₫ Vietnamese (Vietnam) +// 209 | € Basque (Spain) +// 210 | € Breton (France) +// 211 | € Catalan (Spain) +// 212 | € Corsican (France) +// 213 | € Dutch (Belgium) +// 214 | € Dutch (Netherlands) +// 215 | € English (Ireland) +// 216 | € Estonian (Estonia) +// 217 | € Euro (€ 123) +// 218 | € Euro (123 €) +// 219 | € Finnish (Finland) +// 220 | € French (Belgium) +// 221 | € French (France) +// 222 | € French (Luxembourg) +// 223 | € French (Monaco) +// 224 | € French (Réunion) +// 225 | € Galician (Spain) +// 226 | € German (Austria) +// 227 | € German (German) // 228 | € German (Luxembourg) // 229 | € Greek (Greece) // 230 | € Inari Sami (Finland) diff --git a/test/images/excel.bmp b/test/images/excel.bmp new file mode 100755 index 0000000000000000000000000000000000000000..cbd3691abc49097acdc9616678ef2b58c12d83e0 GIT binary patch literal 26678 zcmeI4zi#6;9LHIpfDLphI%(wCC%BpDUf@oX94}PRq3KibJ-iS(qd@Fi@@LA#vFI~& z;3M2{mWK}Nm;6zYY)Q7Skz?r~Iv-P_Wr^S4ACgjX_sg&EerLYC`<}6%vGo^F;5#21_oHh zBm*1>fE}rT1_s!0U5;7+2^7%40LvIQndU$M2^7%40EA>mNEP=%YgtA zD4>A>mI;P#G7j8J0tGZMz>+l1Q41h}0vZ@#Nw(mq1&}}i4Ggek63I~uAb|oJ7+}d< zilY`l0tGZMz>+C7M=gK^3TR+}B{Lh2S^xA>mcra{ z)B;GLfM%cn&Dj#iDV&e7-2bpVcUu52*W~ucGFo7?z-WQ5x4<;^WwFijJd3wDK^)_4 z8)w*Ee*O6_^^Iw~*rF_4pS_Mr98U(R!}%c3m*~CMv79dU9A;y+LhfkdeMu7eoj8}`rB97-%%5#_R2rTjkjb$$HdtVA2 zE=!Of2rE}lLR@7ilX(n{eJ@i#XTSvz0v#@_Twqo6!+$mq9r=9Wb%6P>Edl=1d%ZLcs1*$TrDrBBcIIDZdBkb>Gh@jk!PKWa3@N`{mR>C`?gaZv#YmP zKv(?Kv5rd(Sl^0{uH))b{up&MCf3#HkJpWKV_c1K^|}aZ{`PkEp|Lbt`*`EU^88ba z4v0J6>-rIw)6wV+YOHtjXiG13{11;@OvmGY2TsNsG+9Z`7K{Dy1j;Er_`!$((M?u(;vmW|mzx$Q_ zb|qi!ko4?vRq1zqvrS1wNBv99E$3dhH(#}AZ4oj!9cg%mwU;IX)X}J9kUAQa3{=OS zlAQ^p$fRTSo94Nn^^!zZI#pzbPZZkezm7B|C8ZFEa-C1pq#U|>uS2G^PTahBL`EBG zN+G3C(t90gw4s~a>nKE4bW06Sk#kWH(tjN?p}#Pk`fel2qGBKJaK$U>zYatX*?&zA ziB^05@k+2xEEkHT@A&(hj#p_Keepe?`V1-DvkC5@-p+=gqv*PKNVN8%GB;jT$`Ev< z(KYoQAtngN(P~$7_*+DG`OQk`hn>PUdYlYC3m=~W2X$mAB7t>KR)>ggJbr} zKy+XZ_X1Yl9W-+7DjC4I>pGms%3!~$xdU@Z|K|^H_>kbvE4x!iaLZ71$Rw<@@~oER zP6~9C6MDHe4l0|gii>^JcKz0P)##6KnR=4D<7vG}@9E7cyhGjcBq_*Dm4^R4@=S7mca0++lt6HiL zz#j>%a^Vh`o$l5%f0J7{z}5ZNA;W_P%I>i8t)+VZb%fl`q+d4S!JMBmulFLW_c}sq z=TD*INqBs+^T4G4aaBZBaEE)`OAaY?g(T^_KjfPJiNX3Mj*^vc*|@YXOS!yCd%C^~ zzc4Sm>>s!94_N)zQ3^P;HR`C`F?73m$n343jsED?36-<8+WgQ5FL#4!U&pIMy)Qo- zbyOmu+s(Vu%BOEtCcpcKQ_Az%pOyJm&FE+`*V&sV-LBgo&)(s-3#?8P z(v;Qpw&&sxR@$z1zV>W&e@b~isn9R$&ez#PXU?__H@vkw;PvDBSb2OL!hZyN$Q0ve OqXk9_j21X%f&T!vbW Date: Tue, 7 Feb 2023 00:08:11 +0800 Subject: [PATCH 154/213] This closes #1462 and closes #1464 - Support creating a conditional format with a "stop if true" rule - Support set border color and create solid color for the color data bar - Fix incorrect cell type when modifying string cell with the time number - Update unit test for the add pivot table to avoid pivot table range overlap --- cell.go | 2 +- numfmt.go | 2 +- pivotTable_test.go | 2 +- styles.go | 259 +++++++++++++++++++++++++++++++++++---------- styles_test.go | 22 ++-- xmlDrawing.go | 27 ++--- xmlWorksheet.go | 126 ++++++++++++++++++---- 7 files changed, 339 insertions(+), 101 deletions(-) diff --git a/cell.go b/cell.go index 38366e4a5e..a263b84d8b 100644 --- a/cell.go +++ b/cell.go @@ -527,7 +527,7 @@ func (c *xlsxC) setCellDefault(value string) { c.T, c.V, c.IS = value, value, nil return } - c.V = value + c.T, c.V = "", value } // getCellDate parse cell value which contains a date in the ISO 8601 format. diff --git a/numfmt.go b/numfmt.go index dcad4db20e..af95b54073 100644 --- a/numfmt.go +++ b/numfmt.go @@ -305,7 +305,7 @@ var ( monthNamesYiSuffix = []string{"\ua2cd\ua1aa", "\ua44d\ua1aa", "\ua315\ua1aa", "\ua1d6\ua1aa", "\ua26c\ua1aa", "\ua0d8\ua1aa", "\ua3c3\ua1aa", "\ua246\ua1aa", "\ua22c\ua1aa", "\ua2b0\ua1aa", "\ua2b0\ua2aa\ua1aa", "\ua2b0\ua44b\ua1aa"} // monthNamesZulu list the month names in the Zulu. monthNamesZulu = []string{"Januwari", "Febhuwari", "Mashi", "Ephreli", "Meyi", "Juni", "Julayi", "Agasti", "Septemba", "Okthoba", "Novemba", "Disemba"} - // monthNamesZuluAbbr list teh month name abbreviations in Zulu + // monthNamesZuluAbbr list the month name abbreviations in Zulu monthNamesZuluAbbr = []string{"Jan", "Feb", "Mas", "Eph", "Mey", "Jun", "Jul", "Agas", "Sep", "Okt", "Nov", "Dis"} // apFmtAfrikaans defined the AM/PM name in the Afrikaans. apFmtAfrikaans = "vm./nm." diff --git a/pivotTable_test.go b/pivotTable_test.go index fbf60b36b3..520d56dee1 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -70,7 +70,7 @@ func TestAddPivotTable(t *testing.T) { })) assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet1!$G$37:$W$50", + PivotTableRange: "Sheet1!$G$39:$W$52", Rows: []PivotTableField{{Data: "Month"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}}, diff --git a/styles.go b/styles.go index 5a77bc7c7b..b5752fcebb 100644 --- a/styles.go +++ b/styles.go @@ -3190,8 +3190,21 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // MaxColor - Same as MinColor, see above. // // BarColor - Used for data_bar. Same as MinColor, see above. +// +// BarBorderColor - Used for sets the color for the border line of a data bar, +// this is only visible in Excel 2010 and later. +// +// BarOnly - Used for displays a bar data but not the data in the cells. +// +// BarSolid - Used for turns on a solid (non-gradient) fill for data bars, this +// is only visible in Excel 2010 and later. +// +// StopIfTrue - used to set the "stop if true" feature of a conditional +// formatting rule when more than one rule is applied to a cell or a range of +// cells. When this parameter is set then subsequent rules are not evaluated +// if the current rule is true. func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFormatOptions) error { - drawContFmtFunc := map[string]func(p int, ct string, fmtCond *ConditionalFormatOptions) *xlsxCfRule{ + drawContFmtFunc := map[string]func(p int, ct, GUID string, fmtCond *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule){ "cellIs": drawCondFmtCellIs, "top10": drawCondFmtTop10, "aboveAverage": drawCondFmtAboveAverage, @@ -3207,6 +3220,12 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo if err != nil { return err } + // Create a pseudo GUID for each unique rule. + var rules int + for _, cf := range ws.ConditionalFormatting { + rules += len(cf.CfRule) + } + GUID := fmt.Sprintf("{00000000-0000-0000-%04X-%012X}", f.getSheetID(sheet), rules) var cfRule []*xlsxCfRule for p, v := range opts { var vt, ct string @@ -3219,7 +3238,14 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo if ok || vt == "expression" { drawFunc, ok := drawContFmtFunc[vt] if ok { - cfRule = append(cfRule, drawFunc(p, ct, &v)) + rule, x14rule := drawFunc(p, ct, GUID, &v) + if x14rule != nil { + if err = f.appendCfRule(ws, x14rule); err != nil { + return err + } + f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14) + } + cfRule = append(cfRule, rule) } } } @@ -3232,11 +3258,64 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo return err } +// appendCfRule provides a function to append rules to conditional formatting. +func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error { + var ( + err error + idx int + decodeExtLst *decodeWorksheetExt + condFmts *xlsxX14ConditionalFormattings + decodeCondFmts *decodeX14ConditionalFormattings + ext *xlsxWorksheetExt + condFmtBytes, condFmtsBytes, extLstBytes, extBytes []byte + ) + if ws.ExtLst != nil { // append mode ext + decodeExtLst = new(decodeWorksheetExt) + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). + Decode(decodeExtLst); err != nil && err != io.EOF { + return err + } + for idx, ext = range decodeExtLst.Ext { + if ext.URI == ExtURIConditionalFormattings { + decodeCondFmts = new(decodeX14ConditionalFormattings) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeCondFmts) + condFmtBytes, _ = xml.Marshal([]*xlsxX14ConditionalFormatting{ + { + XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, + CfRule: []*xlsxX14CfRule{rule}, + }, + }) + if condFmts == nil { + condFmts = &xlsxX14ConditionalFormattings{} + } + condFmts.Content = decodeCondFmts.Content + string(condFmtBytes) + condFmtsBytes, _ = xml.Marshal(condFmts) + decodeExtLst.Ext[idx].Content = string(condFmtsBytes) + } + } + extLstBytes, _ = xml.Marshal(decodeExtLst) + ws.ExtLst = &xlsxExtLst{ + Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), ""), + } + return err + } + condFmtBytes, _ = xml.Marshal([]*xlsxX14ConditionalFormatting{ + {XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, CfRule: []*xlsxX14CfRule{rule}}, + }) + condFmtsBytes, _ = xml.Marshal(&xlsxX14ConditionalFormattings{Content: string(condFmtBytes)}) + extBytes, err = xml.Marshal(&xlsxWorksheetExt{ + URI: ExtURIConditionalFormattings, + Content: string(condFmtsBytes), + }) + ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extBytes), ""), "")} + return err +} + // extractCondFmtCellIs provides a function to extract conditional format // settings for cell value (include between, not between, equal, not equal, // greater than and less than) by given conditional formatting rule. -func extractCondFmtCellIs(c *xlsxCfRule) ConditionalFormatOptions { - format := ConditionalFormatOptions{Type: "cell", Criteria: operatorType[c.Operator], Format: *c.DxfID} +func extractCondFmtCellIs(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "cell", Criteria: operatorType[c.Operator], Format: *c.DxfID} if len(c.Formula) == 2 { format.Minimum, format.Maximum = c.Formula[0], c.Formula[1] return format @@ -3248,13 +3327,14 @@ func extractCondFmtCellIs(c *xlsxCfRule) ConditionalFormatOptions { // extractCondFmtTop10 provides a function to extract conditional format // settings for top N (default is top 10) by given conditional formatting // rule. -func extractCondFmtTop10(c *xlsxCfRule) ConditionalFormatOptions { +func extractCondFmtTop10(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{ - Type: "top", - Criteria: "=", - Format: *c.DxfID, - Percent: c.Percent, - Value: strconv.Itoa(c.Rank), + StopIfTrue: c.StopIfTrue, + Type: "top", + Criteria: "=", + Format: *c.DxfID, + Percent: c.Percent, + Value: strconv.Itoa(c.Rank), } if c.Bottom { format.Type = "bottom" @@ -3265,8 +3345,9 @@ func extractCondFmtTop10(c *xlsxCfRule) ConditionalFormatOptions { // extractCondFmtAboveAverage provides a function to extract conditional format // settings for above average and below average by given conditional formatting // rule. -func extractCondFmtAboveAverage(c *xlsxCfRule) ConditionalFormatOptions { +func extractCondFmtAboveAverage(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { return ConditionalFormatOptions{ + StopIfTrue: c.StopIfTrue, Type: "average", Criteria: "=", Format: *c.DxfID, @@ -3277,8 +3358,9 @@ func extractCondFmtAboveAverage(c *xlsxCfRule) ConditionalFormatOptions { // extractCondFmtDuplicateUniqueValues provides a function to extract // conditional format settings for duplicate and unique values by given // conditional formatting rule. -func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule) ConditionalFormatOptions { +func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { return ConditionalFormatOptions{ + StopIfTrue: c.StopIfTrue, Type: map[string]string{ "duplicateValues": "duplicate", "uniqueValues": "unique", @@ -3291,8 +3373,8 @@ func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule) ConditionalFormatOptions // extractCondFmtColorScale provides a function to extract conditional format // settings for color scale (include 2 color scale and 3 color scale) by given // conditional formatting rule. -func extractCondFmtColorScale(c *xlsxCfRule) ConditionalFormatOptions { - var format ConditionalFormatOptions +func extractCondFmtColorScale(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue} format.Type, format.Criteria = "2_color_scale", "=" values := len(c.ColorScale.Cfvo) colors := len(c.ColorScale.Color) @@ -3326,20 +3408,58 @@ func extractCondFmtColorScale(c *xlsxCfRule) ConditionalFormatOptions { // extractCondFmtDataBar provides a function to extract conditional format // settings for data bar by given conditional formatting rule. -func extractCondFmtDataBar(c *xlsxCfRule) ConditionalFormatOptions { +func extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{Type: "data_bar", Criteria: "="} if c.DataBar != nil { + format.StopIfTrue = c.StopIfTrue format.MinType = c.DataBar.Cfvo[0].Type format.MaxType = c.DataBar.Cfvo[1].Type format.BarColor = "#" + strings.TrimPrefix(strings.ToUpper(c.DataBar.Color[0].RGB), "FF") + if c.DataBar.ShowValue != nil { + format.BarOnly = !*c.DataBar.ShowValue + } + } + extractDataBarRule := func(condFmts []decodeX14ConditionalFormatting) { + for _, condFmt := range condFmts { + for _, rule := range condFmt.CfRule { + if rule.DataBar != nil { + format.BarSolid = !rule.DataBar.Gradient + if rule.DataBar.BorderColor != nil { + format.BarBorderColor = "#" + strings.TrimPrefix(strings.ToUpper(rule.DataBar.BorderColor.RGB), "FF") + } + } + } + } + } + extractExtLst := func(extLst *decodeWorksheetExt) { + for _, ext := range extLst.Ext { + if ext.URI == ExtURIConditionalFormattings { + decodeCondFmts := new(decodeX14ConditionalFormattings) + if err := xml.Unmarshal([]byte(ext.Content), &decodeCondFmts); err == nil { + condFmts := []decodeX14ConditionalFormatting{} + if err = xml.Unmarshal([]byte(decodeCondFmts.Content), &condFmts); err == nil { + extractDataBarRule(condFmts) + } + } + } + } + } + if c.ExtLst != nil { + ext := decodeX14ConditionalFormattingExt{} + if err := xml.Unmarshal([]byte(c.ExtLst.Ext), &ext); err == nil && extLst != nil { + decodeExtLst := new(decodeWorksheetExt) + if err = xml.Unmarshal([]byte(""+extLst.Ext+""), decodeExtLst); err == nil { + extractExtLst(decodeExtLst) + } + } } return format } // extractCondFmtExp provides a function to extract conditional format settings // for expression by given conditional formatting rule. -func extractCondFmtExp(c *xlsxCfRule) ConditionalFormatOptions { - format := ConditionalFormatOptions{Type: "formula", Format: *c.DxfID} +func extractCondFmtExp(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "formula", Format: *c.DxfID} if len(c.Formula) > 0 { format.Criteria = c.Formula[0] } @@ -3349,7 +3469,7 @@ func extractCondFmtExp(c *xlsxCfRule) ConditionalFormatOptions { // GetConditionalFormats returns conditional format settings by given worksheet // name. func (f *File) GetConditionalFormats(sheet string) (map[string][]ConditionalFormatOptions, error) { - extractContFmtFunc := map[string]func(c *xlsxCfRule) ConditionalFormatOptions{ + extractContFmtFunc := map[string]func(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions{ "cellIs": extractCondFmtCellIs, "top10": extractCondFmtTop10, "aboveAverage": extractCondFmtAboveAverage, @@ -3369,7 +3489,7 @@ func (f *File) GetConditionalFormats(sheet string) (map[string][]ConditionalForm var opts []ConditionalFormatOptions for _, cr := range cf.CfRule { if extractFunc, ok := extractContFmtFunc[cr.Type]; ok { - opts = append(opts, extractFunc(cr)) + opts = append(opts, extractFunc(cr, ws.ExtLst)) } } conditionalFormats[cf.SQRef] = opts @@ -3396,12 +3516,13 @@ func (f *File) UnsetConditionalFormat(sheet, rangeRef string) error { // drawCondFmtCellIs provides a function to create conditional formatting rule // for cell value (include between, not between, equal, not equal, greater // than and less than) by given priority, criteria type and format settings. -func drawCondFmtCellIs(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { +func drawCondFmtCellIs(p int, ct, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { c := &xlsxCfRule{ - Priority: p + 1, - Type: validType[format.Type], - Operator: ct, - DxfID: &format.Format, + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Type: validType[format.Type], + Operator: ct, + DxfID: &format.Format, } // "between" and "not between" criteria require 2 values. if ct == "between" || ct == "notBetween" { @@ -3410,54 +3531,57 @@ func drawCondFmtCellIs(p int, ct string, format *ConditionalFormatOptions) *xlsx if idx := inStrSlice([]string{"equal", "notEqual", "greaterThan", "lessThan", "greaterThanOrEqual", "lessThanOrEqual", "containsText", "notContains", "beginsWith", "endsWith"}, ct, true); idx != -1 { c.Formula = append(c.Formula, format.Value) } - return c + return c, nil } // drawCondFmtTop10 provides a function to create conditional formatting rule // for top N (default is top 10) by given priority, criteria type and format // settings. -func drawCondFmtTop10(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { +func drawCondFmtTop10(p int, ct, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { c := &xlsxCfRule{ - Priority: p + 1, - Bottom: format.Type == "bottom", - Type: validType[format.Type], - Rank: 10, - DxfID: &format.Format, - Percent: format.Percent, + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Bottom: format.Type == "bottom", + Type: validType[format.Type], + Rank: 10, + DxfID: &format.Format, + Percent: format.Percent, } if rank, err := strconv.Atoi(format.Value); err == nil { c.Rank = rank } - return c + return c, nil } // drawCondFmtAboveAverage provides a function to create conditional // formatting rule for above average and below average by given priority, // criteria type and format settings. -func drawCondFmtAboveAverage(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { +func drawCondFmtAboveAverage(p int, ct, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { return &xlsxCfRule{ Priority: p + 1, + StopIfTrue: format.StopIfTrue, Type: validType[format.Type], AboveAverage: &format.AboveAverage, DxfID: &format.Format, - } + }, nil } // drawCondFmtDuplicateUniqueValues provides a function to create conditional // formatting rule for duplicate and unique values by given priority, criteria // type and format settings. -func drawCondFmtDuplicateUniqueValues(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { +func drawCondFmtDuplicateUniqueValues(p int, ct, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { return &xlsxCfRule{ - Priority: p + 1, - Type: validType[format.Type], - DxfID: &format.Format, - } + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Type: validType[format.Type], + DxfID: &format.Format, + }, nil } // drawCondFmtColorScale provides a function to create conditional formatting // rule for color scale (include 2 color scale and 3 color scale) by given // priority, criteria type and format settings. -func drawCondFmtColorScale(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { +func drawCondFmtColorScale(p int, ct, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { minValue := format.MinValue if minValue == "" { minValue = "0" @@ -3472,8 +3596,9 @@ func drawCondFmtColorScale(p int, ct string, format *ConditionalFormatOptions) * } c := &xlsxCfRule{ - Priority: p + 1, - Type: "colorScale", + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Type: "colorScale", ColorScale: &xlsxColorScale{ Cfvo: []*xlsxCfvo{ {Type: format.MinType, Val: minValue}, @@ -3489,31 +3614,53 @@ func drawCondFmtColorScale(p int, ct string, format *ConditionalFormatOptions) * } c.ColorScale.Cfvo = append(c.ColorScale.Cfvo, &xlsxCfvo{Type: format.MaxType, Val: maxValue}) c.ColorScale.Color = append(c.ColorScale.Color, &xlsxColor{RGB: getPaletteColor(format.MaxColor)}) - return c + return c, nil } // drawCondFmtDataBar provides a function to create conditional formatting // rule for data bar by given priority, criteria type and format settings. -func drawCondFmtDataBar(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { +func drawCondFmtDataBar(p int, ct, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { + var x14CfRule *xlsxX14CfRule + var extLst *xlsxExtLst + if format.BarSolid { + extLst = &xlsxExtLst{Ext: fmt.Sprintf(`%s`, ExtURIConditionalFormattingRuleID, NameSpaceSpreadSheetX14.Value, GUID)} + x14CfRule = &xlsxX14CfRule{ + Type: validType[format.Type], + ID: GUID, + DataBar: &xlsx14DataBar{ + MaxLength: 100, + Cfvo: []*xlsxCfvo{{Type: "autoMin"}, {Type: "autoMax"}}, + NegativeFillColor: &xlsxColor{RGB: "FFFF0000"}, + AxisColor: &xlsxColor{RGB: "FFFF0000"}, + }, + } + if format.BarBorderColor != "" { + x14CfRule.DataBar.BorderColor = &xlsxColor{RGB: getPaletteColor(format.BarBorderColor)} + } + } return &xlsxCfRule{ - Priority: p + 1, - Type: validType[format.Type], + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Type: validType[format.Type], DataBar: &xlsxDataBar{ - Cfvo: []*xlsxCfvo{{Type: format.MinType}, {Type: format.MaxType}}, - Color: []*xlsxColor{{RGB: getPaletteColor(format.BarColor)}}, + ShowValue: boolPtr(!format.BarOnly), + Cfvo: []*xlsxCfvo{{Type: format.MinType}, {Type: format.MaxType}}, + Color: []*xlsxColor{{RGB: getPaletteColor(format.BarColor)}}, }, - } + ExtLst: extLst, + }, x14CfRule } // drawCondFmtExp provides a function to create conditional formatting rule // for expression by given priority, criteria type and format settings. -func drawCondFmtExp(p int, ct string, format *ConditionalFormatOptions) *xlsxCfRule { +func drawCondFmtExp(p int, ct, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { return &xlsxCfRule{ - Priority: p + 1, - Type: validType[format.Type], - Formula: []string{format.Criteria}, - DxfID: &format.Format, - } + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Type: validType[format.Type], + Formula: []string{format.Criteria}, + DxfID: &format.Format, + }, nil } // getPaletteColor provides a function to convert the RBG color by given diff --git a/styles_test.go b/styles_test.go index 86860fad59..cd90a3c94c 100644 --- a/styles_test.go +++ b/styles_test.go @@ -159,12 +159,7 @@ func TestSetConditionalFormat(t *testing.T) { f := NewFile() const sheet = "Sheet1" const rangeRef = "A1:A1" - - err := f.SetConditionalFormat(sheet, rangeRef, testCase.format) - if err != nil { - t.Fatalf("%s", err) - } - + assert.NoError(t, f.SetConditionalFormat(sheet, rangeRef, testCase.format)) ws, err := f.workSheetReader(sheet) assert.NoError(t, err) cf := ws.ConditionalFormatting @@ -173,6 +168,19 @@ func TestSetConditionalFormat(t *testing.T) { assert.Equal(t, rangeRef, cf[0].SQRef, testCase.label) assert.EqualValues(t, testCase.rules, cf[0].CfRule, testCase.label) } + // Test creating a conditional format with a solid color data bar style + f := NewFile() + condFmts := []ConditionalFormatOptions{ + {Type: "data_bar", BarColor: "#A9D08E", BarSolid: true, Format: 0, Criteria: "=", MinType: "min", MaxType: "max"}, + } + for _, ref := range []string{"A1:A2", "B1:B2"} { + assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, condFmts)) + } + // Test creating a conditional format with invalid extension list characters + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).ExtLst.Ext = "" + assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", condFmts), "XML syntax error on line 1: element closed by ") } func TestGetConditionalFormats(t *testing.T) { @@ -186,7 +194,7 @@ func TestGetConditionalFormats(t *testing.T) { {{Type: "unique", Format: 1, Criteria: "="}}, {{Type: "3_color_scale", Criteria: "=", MinType: "num", MidType: "num", MaxType: "num", MinValue: "-10", MidValue: "50", MaxValue: "10", MinColor: "#FF0000", MidColor: "#00FF00", MaxColor: "#0000FF"}}, {{Type: "2_color_scale", Criteria: "=", MinType: "num", MaxType: "num", MinColor: "#FF0000", MaxColor: "#0000FF"}}, - {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarColor: "#638EC6"}}, + {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarColor: "#638EC6", BarBorderColor: "#0000FF", BarOnly: true, BarSolid: true, StopIfTrue: true}}, {{Type: "formula", Format: 1, Criteria: "="}}, } { f := NewFile() diff --git a/xmlDrawing.go b/xmlDrawing.go index a0fcfb0309..56ba3ebcd3 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -91,19 +91,20 @@ const ( // ([ISO/IEC29500-1:2016] section 18.2.10) of the worksheet element // ([ISO/IEC29500-1:2016] section 18.3.1.99) is extended by the addition of // new child ext elements ([ISO/IEC29500-1:2016] section 18.2.7) - ExtURIConditionalFormattings = "{78C0D931-6437-407D-A8EE-F0AAD7539E65}" - ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}" - ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}" - ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}" - ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}" - ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}" - ExtURISlicerCachesListX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}" - ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" - ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}" - ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" - ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}" - ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}" - ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}" + ExtURIConditionalFormattingRuleID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}" + ExtURIConditionalFormattings = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}" + ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}" + ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}" + ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}" + ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}" + ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}" + ExtURISlicerCachesListX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}" + ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" + ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}" + ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" + ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}" + ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}" + ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}" ) // Excel specifications and limits diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 8b45a34e5c..07000bd4ae 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -592,7 +592,7 @@ type xlsxColorScale struct { type xlsxDataBar struct { MaxLength int `xml:"maxLength,attr,omitempty"` MinLength int `xml:"minLength,attr,omitempty"` - ShowValue bool `xml:"showValue,attr,omitempty"` + ShowValue *bool `xml:"showValue,attr"` Cfvo []*xlsxCfvo `xml:"cfvo"` Color []*xlsxColor `xml:"color"` } @@ -601,7 +601,7 @@ type xlsxDataBar struct { type xlsxIconSet struct { Cfvo []*xlsxCfvo `xml:"cfvo"` IconSet string `xml:"iconSet,attr,omitempty"` - ShowValue bool `xml:"showValue,attr,omitempty"` + ShowValue *bool `xml:"showValue,attr"` Percent bool `xml:"percent,attr,omitempty"` Reverse bool `xml:"reverse,attr,omitempty"` } @@ -742,6 +742,84 @@ type decodeX14SparklineGroups struct { Content string `xml:",innerxml"` } +// decodeX14ConditionalFormattingExt directly maps the ext +// element. +type decodeX14ConditionalFormattingExt struct { + XMLName xml.Name `xml:"ext"` + ID string `xml:"id"` +} + +// decodeX14ConditionalFormattings directly maps the conditionalFormattings +// element. +type decodeX14ConditionalFormattings struct { + XMLName xml.Name `xml:"conditionalFormattings"` + XMLNSXM string `xml:"xmlns:xm,attr"` + Content string `xml:",innerxml"` +} + +// decodeX14ConditionalFormatting directly maps the conditionalFormatting +// element. +type decodeX14ConditionalFormatting struct { + XMLName xml.Name `xml:"conditionalFormatting"` + CfRule []*decodeX14CfRule `xml:"cfRule"` +} + +// decodeX14CfRule directly maps the cfRule element. +type decodeX14CfRule struct { + XNLName xml.Name `xml:"cfRule"` + Type string `xml:"type,attr,omitempty"` + ID string `xml:"id,attr,omitempty"` + DataBar *decodeX14DataBar `xml:"dataBar"` +} + +// decodeX14DataBar directly maps the dataBar element. +type decodeX14DataBar struct { + XNLName xml.Name `xml:"dataBar"` + MaxLength int `xml:"maxLength,attr"` + MinLength int `xml:"minLength,attr"` + Gradient bool `xml:"gradient,attr"` + ShowValue bool `xml:"showValue,attr,omitempty"` + Cfvo []*xlsxCfvo `xml:"cfvo"` + BorderColor *xlsxColor `xml:"borderColor"` + NegativeFillColor *xlsxColor `xml:"negativeFillColor"` + AxisColor *xlsxColor `xml:"axisColor"` +} + +// xlsxX14ConditionalFormattings directly maps the conditionalFormattings +// element. +type xlsxX14ConditionalFormattings struct { + XMLName xml.Name `xml:"x14:conditionalFormattings"` + Content string `xml:",innerxml"` +} + +// xlsxX14ConditionalFormatting directly maps the conditionalFormatting element. +type xlsxX14ConditionalFormatting struct { + XMLName xml.Name `xml:"x14:conditionalFormatting"` + XMLNSXM string `xml:"xmlns:xm,attr"` + CfRule []*xlsxX14CfRule `xml:"x14:cfRule"` +} + +// xlsxX14CfRule directly maps the cfRule element. +type xlsxX14CfRule struct { + XNLName xml.Name `xml:"x14:cfRule"` + Type string `xml:"type,attr,omitempty"` + ID string `xml:"id,attr,omitempty"` + DataBar *xlsx14DataBar `xml:"x14:dataBar"` +} + +// xlsx14DataBar directly maps the dataBar element. +type xlsx14DataBar struct { + XNLName xml.Name `xml:"x14:dataBar"` + MaxLength int `xml:"maxLength,attr"` + MinLength int `xml:"minLength,attr"` + Gradient bool `xml:"gradient,attr"` + ShowValue bool `xml:"showValue,attr,omitempty"` + Cfvo []*xlsxCfvo `xml:"x14:cfvo"` + BorderColor *xlsxColor `xml:"x14:borderColor"` + NegativeFillColor *xlsxColor `xml:"x14:negativeFillColor"` + AxisColor *xlsxColor `xml:"x14:axisColor"` +} + // xlsxX14SparklineGroups directly maps the sparklineGroups element. type xlsxX14SparklineGroups struct { XMLName xml.Name `xml:"x14:sparklineGroups"` @@ -843,26 +921,30 @@ type Panes struct { // ConditionalFormatOptions directly maps the conditional format settings of the cells. type ConditionalFormatOptions struct { - Type string - AboveAverage bool - Percent bool - Format int - Criteria string - Value string - Minimum string - Maximum string - MinType string - MidType string - MaxType string - MinValue string - MidValue string - MaxValue string - MinColor string - MidColor string - MaxColor string - MinLength string - MaxLength string - BarColor string + Type string + AboveAverage bool + Percent bool + Format int + Criteria string + Value string + Minimum string + Maximum string + MinType string + MidType string + MaxType string + MinValue string + MidValue string + MaxValue string + MinColor string + MidColor string + MaxColor string + MinLength string + MaxLength string + BarColor string + BarBorderColor string + BarOnly bool + BarSolid bool + StopIfTrue bool } // SheetProtectionOptions directly maps the settings of worksheet protection. From 753969dc4efee833d14c2fb537200ef14849571f Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 8 Feb 2023 00:03:45 +0800 Subject: [PATCH 155/213] Support creating a conditional format with an "icon sets" rule - Improvement compatibility for the worksheet extension lists - Update unit test --- sparkline.go | 105 ++++++++++++-------------- sparkline_test.go | 24 +++--- styles.go | 183 +++++++++++++++++++++++++++++++++++++--------- styles_test.go | 19 ++++- xmlDrawing.go | 12 +++ xmlWorksheet.go | 10 ++- 6 files changed, 245 insertions(+), 108 deletions(-) diff --git a/sparkline.go b/sparkline.go index 43a827e151..b9879ac81d 100644 --- a/sparkline.go +++ b/sparkline.go @@ -14,6 +14,7 @@ package excelize import ( "encoding/xml" "io" + "sort" "strings" ) @@ -389,15 +390,14 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // Axis | Show sparkline axis func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error { var ( - err error - ws *xlsxWorksheet - sparkType string - sparkTypes map[string]string - specifiedSparkTypes string - ok bool - group *xlsxX14SparklineGroup - groups *xlsxX14SparklineGroups - sparklineGroupsBytes, extBytes []byte + err error + ws *xlsxWorksheet + sparkType string + sparkTypes map[string]string + specifiedSparkTypes string + ok bool + group *xlsxX14SparklineGroup + groups *xlsxX14SparklineGroups ) // parameter validation @@ -434,25 +434,8 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error { group.RightToLeft = opts.Reverse } f.addSparkline(opts, group) - if ws.ExtLst.Ext != "" { // append mode ext - if err = f.appendSparkline(ws, group, groups); err != nil { - return err - } - } else { - groups = &xlsxX14SparklineGroups{ - XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, - SparklineGroups: []*xlsxX14SparklineGroup{group}, - } - if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { - return err - } - if extBytes, err = xml.Marshal(&xlsxWorksheetExt{ - URI: ExtURISparklineGroups, - Content: string(sparklineGroupsBytes), - }); err != nil { - return err - } - ws.ExtLst.Ext = string(extBytes) + if err = f.appendSparkline(ws, group, groups); err != nil { + return err } f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14) return err @@ -504,42 +487,50 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, var ( err error idx int - decodeExtLst *decodeWorksheetExt + appendMode bool + decodeExtLst = new(decodeWorksheetExt) decodeSparklineGroups *decodeX14SparklineGroups ext *xlsxWorksheetExt sparklineGroupsBytes, sparklineGroupBytes, extLstBytes []byte ) - decodeExtLst = new(decodeWorksheetExt) - if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). - Decode(decodeExtLst); err != nil && err != io.EOF { - return err - } - for idx, ext = range decodeExtLst.Ext { - if ext.URI == ExtURISparklineGroups { - decodeSparklineGroups = new(decodeX14SparklineGroups) - if err = f.xmlNewDecoder(strings.NewReader(ext.Content)). - Decode(decodeSparklineGroups); err != nil && err != io.EOF { - return err - } - if sparklineGroupBytes, err = xml.Marshal(group); err != nil { - return err - } - if groups == nil { - groups = &xlsxX14SparklineGroups{} - } - groups.XMLNSXM = NameSpaceSpreadSheetExcel2006Main.Value - groups.Content = decodeSparklineGroups.Content + string(sparklineGroupBytes) - if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { - return err + sparklineGroupBytes, _ = xml.Marshal(group) + if ws.ExtLst != nil { // append mode ext + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). + Decode(decodeExtLst); err != nil && err != io.EOF { + return err + } + for idx, ext = range decodeExtLst.Ext { + if ext.URI == ExtURISparklineGroups { + decodeSparklineGroups = new(decodeX14SparklineGroups) + if err = f.xmlNewDecoder(strings.NewReader(ext.Content)). + Decode(decodeSparklineGroups); err != nil && err != io.EOF { + return err + } + if groups == nil { + groups = &xlsxX14SparklineGroups{} + } + groups.XMLNSXM = NameSpaceSpreadSheetExcel2006Main.Value + groups.Content = decodeSparklineGroups.Content + string(sparklineGroupBytes) + sparklineGroupsBytes, _ = xml.Marshal(groups) + decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes) + appendMode = true } - decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes) } } - if extLstBytes, err = xml.Marshal(decodeExtLst); err != nil { - return err - } - ws.ExtLst = &xlsxExtLst{ - Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), ""), + if !appendMode { + sparklineGroupsBytes, _ = xml.Marshal(&xlsxX14SparklineGroups{ + XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, + SparklineGroups: []*xlsxX14SparklineGroup{group}, + }) + decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxWorksheetExt{ + URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes), + }) } + sort.Slice(decodeExtLst.Ext, func(i, j int) bool { + return inStrSlice(extensionURIPriority, decodeExtLst.Ext[i].URI, false) < + inStrSlice(extensionURIPriority, decodeExtLst.Ext[j].URI, false) + }) + extLstBytes, err = xml.Marshal(decodeExtLst) + ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} return err } diff --git a/sparkline_test.go b/sparkline_test.go index e6bca25808..b1d3d18672 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -264,23 +264,23 @@ func TestAddSparkline(t *testing.T) { Range: []string{"Sheet2!A3:E3"}, Style: -1, }), ErrSparklineStyle.Error()) - + // Test creating a conditional format with existing extension lists ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) - ws.(*xlsxWorksheet).ExtLst.Ext = ` - - - - - - - - ` + ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: ` + + `} + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + Location: []string{"A3"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + })) + // Test creating a conditional format with invalid extension list characters + ws.(*xlsxWorksheet).ExtLst.Ext = `` assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A2"}, Range: []string{"Sheet3!A1:J1"}, - }), "XML syntax error on line 6: element closed by ") + }), "XML syntax error on line 1: element closed by ") } func TestAppendSparkline(t *testing.T) { diff --git a/styles.go b/styles.go index b5752fcebb..78083f2b07 100644 --- a/styles.go +++ b/styles.go @@ -18,6 +18,7 @@ import ( "io" "math" "reflect" + "sort" "strconv" "strings" ) @@ -807,6 +808,7 @@ var validType = map[string]string{ "3_color_scale": "3_color_scale", "data_bar": "dataBar", "formula": "expression", + "iconSet": "iconSet", } // criteriaType defined the list of valid criteria types. @@ -2880,7 +2882,14 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // | MaxType // | MinValue // | MaxValue +// | BarBorderColor // | BarColor +// | BarDirection +// | BarOnly +// | BarSolid +// iconSet | IconStyle +// | ReverseIcons +// | IconsOnly // formula | Criteria // // The 'Criteria' parameter is used to set the criteria by which the cell data @@ -3037,7 +3046,8 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // }, // ) // -// type: duplicate - The duplicate type is used to highlight duplicate cells in a range: +// type: duplicate - The duplicate type is used to highlight duplicate cells in +// a range: // // // Highlight cells rules: Duplicate Values... // err := f.SetConditionalFormat("Sheet1", "A1:A10", @@ -3055,7 +3065,8 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // }, // ) // -// type: top - The top type is used to specify the top n values by number or percentage in a range: +// type: top - The top type is used to specify the top n values by number or +// percentage in a range: // // // Top/Bottom rules: Top 10. // err := f.SetConditionalFormat("Sheet1", "H1:H10", @@ -3129,7 +3140,10 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // type: data_bar - The data_bar type is used to specify Excel's "Data Bar" // style conditional format. // -// MinType - The MinType and MaxType properties are available when the conditional formatting type is 2_color_scale, 3_color_scale or data_bar. The MidType is available for 3_color_scale. The properties are used as follows: +// MinType - The MinType and MaxType properties are available when the +// conditional formatting type is 2_color_scale, 3_color_scale or data_bar. +// The MidType is available for 3_color_scale. The properties are used as +// follows: // // // Data Bars: Gradient Fill. // err := f.SetConditionalFormat("Sheet1", "K1:K10", @@ -3194,11 +3208,41 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // BarBorderColor - Used for sets the color for the border line of a data bar, // this is only visible in Excel 2010 and later. // -// BarOnly - Used for displays a bar data but not the data in the cells. +// BarDirection - sets the direction for data bars. The available options are: +// +// context - Data bar direction is set by spreadsheet application based on the context of the data displayed. +// leftToRight - Data bar direction is from right to left. +// rightToLeft - Data bar direction is from left to right. +// +// BarOnly - Used for set displays a bar data but not the data in the cells. // // BarSolid - Used for turns on a solid (non-gradient) fill for data bars, this // is only visible in Excel 2010 and later. // +// IconStyle - The available options are: +// +// 3Arrows +// 3ArrowsGray +// 3Flags +// 3Signs +// 3Symbols +// 3Symbols2 +// 3TrafficLights1 +// 3TrafficLights2 +// 4Arrows +// 4ArrowsGray +// 4Rating +// 4RedToBlack +// 4TrafficLights +// 5Arrows +// 5ArrowsGray +// 5Quarters +// 5Rating +// +// ReverseIcons - Used for set reversed icons sets. +// +// IconsOnly - Used for set displayed without the cell value. +// // StopIfTrue - used to set the "stop if true" feature of a conditional // formatting rule when more than one rule is applied to a cell or a range of // cells. When this parameter is set then subsequent rules are not evaluated @@ -3214,6 +3258,7 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo "3_color_scale": drawCondFmtColorScale, "dataBar": drawCondFmtDataBar, "expression": drawCondFmtExp, + "iconSet": drawCondFmtIconSet, } ws, err := f.workSheetReader(sheet) @@ -3235,10 +3280,13 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo if ok { // Check for valid criteria types. ct, ok = criteriaType[v.Criteria] - if ok || vt == "expression" { + if ok || vt == "expression" || vt == "iconSet" { drawFunc, ok := drawContFmtFunc[vt] if ok { rule, x14rule := drawFunc(p, ct, GUID, &v) + if rule == nil { + return ErrParameterInvalid + } if x14rule != nil { if err = f.appendCfRule(ws, x14rule); err != nil { return err @@ -3261,16 +3309,19 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo // appendCfRule provides a function to append rules to conditional formatting. func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error { var ( - err error - idx int - decodeExtLst *decodeWorksheetExt - condFmts *xlsxX14ConditionalFormattings - decodeCondFmts *decodeX14ConditionalFormattings - ext *xlsxWorksheetExt - condFmtBytes, condFmtsBytes, extLstBytes, extBytes []byte + err error + idx int + appendMode bool + decodeExtLst = new(decodeWorksheetExt) + condFmts *xlsxX14ConditionalFormattings + decodeCondFmts *decodeX14ConditionalFormattings + ext *xlsxWorksheetExt + condFmtBytes, condFmtsBytes, extLstBytes []byte ) + condFmtBytes, _ = xml.Marshal([]*xlsxX14ConditionalFormatting{ + {XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, CfRule: []*xlsxX14CfRule{rule}}, + }) if ws.ExtLst != nil { // append mode ext - decodeExtLst = new(decodeWorksheetExt) if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). Decode(decodeExtLst); err != nil && err != io.EOF { return err @@ -3279,35 +3330,28 @@ func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error { if ext.URI == ExtURIConditionalFormattings { decodeCondFmts = new(decodeX14ConditionalFormattings) _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeCondFmts) - condFmtBytes, _ = xml.Marshal([]*xlsxX14ConditionalFormatting{ - { - XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, - CfRule: []*xlsxX14CfRule{rule}, - }, - }) if condFmts == nil { condFmts = &xlsxX14ConditionalFormattings{} } condFmts.Content = decodeCondFmts.Content + string(condFmtBytes) condFmtsBytes, _ = xml.Marshal(condFmts) decodeExtLst.Ext[idx].Content = string(condFmtsBytes) + appendMode = true } } - extLstBytes, _ = xml.Marshal(decodeExtLst) - ws.ExtLst = &xlsxExtLst{ - Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), ""), - } - return err } - condFmtBytes, _ = xml.Marshal([]*xlsxX14ConditionalFormatting{ - {XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, CfRule: []*xlsxX14CfRule{rule}}, - }) - condFmtsBytes, _ = xml.Marshal(&xlsxX14ConditionalFormattings{Content: string(condFmtBytes)}) - extBytes, err = xml.Marshal(&xlsxWorksheetExt{ - URI: ExtURIConditionalFormattings, - Content: string(condFmtsBytes), + if !appendMode { + condFmtsBytes, _ = xml.Marshal(&xlsxX14ConditionalFormattings{Content: string(condFmtBytes)}) + decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxWorksheetExt{ + URI: ExtURIConditionalFormattings, Content: string(condFmtsBytes), + }) + } + sort.Slice(decodeExtLst.Ext, func(i, j int) bool { + return inStrSlice(extensionURIPriority, decodeExtLst.Ext[i].URI, false) < + inStrSlice(extensionURIPriority, decodeExtLst.Ext[j].URI, false) }) - ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extBytes), ""), "")} + extLstBytes, err = xml.Marshal(decodeExtLst) + ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} return err } @@ -3424,6 +3468,7 @@ func extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatO for _, rule := range condFmt.CfRule { if rule.DataBar != nil { format.BarSolid = !rule.DataBar.Gradient + format.BarDirection = rule.DataBar.Direction if rule.DataBar.BorderColor != nil { format.BarBorderColor = "#" + strings.TrimPrefix(strings.ToUpper(rule.DataBar.BorderColor.RGB), "FF") } @@ -3466,6 +3511,20 @@ func extractCondFmtExp(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptio return format } +// extractCondFmtIconSet provides a function to extract conditional format +// settings for icon sets by given conditional formatting rule. +func extractCondFmtIconSet(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + format := ConditionalFormatOptions{Type: "iconSet"} + if c.IconSet != nil { + if c.IconSet.ShowValue != nil { + format.IconsOnly = !*c.IconSet.ShowValue + } + format.IconStyle = c.IconSet.IconSet + format.ReverseIcons = c.IconSet.Reverse + } + return format +} + // GetConditionalFormats returns conditional format settings by given worksheet // name. func (f *File) GetConditionalFormats(sheet string) (map[string][]ConditionalFormatOptions, error) { @@ -3478,6 +3537,7 @@ func (f *File) GetConditionalFormats(sheet string) (map[string][]ConditionalForm "colorScale": extractCondFmtColorScale, "dataBar": extractCondFmtDataBar, "expression": extractCondFmtExp, + "iconSet": extractCondFmtIconSet, } conditionalFormats := make(map[string][]ConditionalFormatOptions) @@ -3622,19 +3682,22 @@ func drawCondFmtColorScale(p int, ct, GUID string, format *ConditionalFormatOpti func drawCondFmtDataBar(p int, ct, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { var x14CfRule *xlsxX14CfRule var extLst *xlsxExtLst - if format.BarSolid { + if format.BarSolid || format.BarDirection == "leftToRight" || format.BarDirection == "rightToLeft" || format.BarBorderColor != "" { extLst = &xlsxExtLst{Ext: fmt.Sprintf(`%s`, ExtURIConditionalFormattingRuleID, NameSpaceSpreadSheetX14.Value, GUID)} x14CfRule = &xlsxX14CfRule{ Type: validType[format.Type], ID: GUID, DataBar: &xlsx14DataBar{ MaxLength: 100, + Border: format.BarBorderColor != "", + Gradient: !format.BarSolid, + Direction: format.BarDirection, Cfvo: []*xlsxCfvo{{Type: "autoMin"}, {Type: "autoMax"}}, NegativeFillColor: &xlsxColor{RGB: "FFFF0000"}, AxisColor: &xlsxColor{RGB: "FFFF0000"}, }, } - if format.BarBorderColor != "" { + if x14CfRule.DataBar.Border { x14CfRule.DataBar.BorderColor = &xlsxColor{RGB: getPaletteColor(format.BarBorderColor)} } } @@ -3663,6 +3726,58 @@ func drawCondFmtExp(p int, ct, GUID string, format *ConditionalFormatOptions) (* }, nil } +// drawCondFmtIconSet provides a function to create conditional formatting rule +// for icon set by given priority, criteria type and format settings. +func drawCondFmtIconSet(p int, ct, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { + cfvo3 := &xlsxCfRule{IconSet: &xlsxIconSet{Cfvo: []*xlsxCfvo{ + {Type: "percent", Val: "0"}, + {Type: "percent", Val: "33"}, + {Type: "percent", Val: "67"}, + }}} + cfvo4 := &xlsxCfRule{IconSet: &xlsxIconSet{Cfvo: []*xlsxCfvo{ + {Type: "percent", Val: "0"}, + {Type: "percent", Val: "25"}, + {Type: "percent", Val: "50"}, + {Type: "percent", Val: "75"}, + }}} + cfvo5 := &xlsxCfRule{IconSet: &xlsxIconSet{Cfvo: []*xlsxCfvo{ + {Type: "percent", Val: "0"}, + {Type: "percent", Val: "20"}, + {Type: "percent", Val: "40"}, + {Type: "percent", Val: "60"}, + {Type: "percent", Val: "80"}, + }}} + presets := map[string]*xlsxCfRule{ + "3Arrows": cfvo3, + "3ArrowsGray": cfvo3, + "3Flags": cfvo3, + "3Signs": cfvo3, + "3Symbols": cfvo3, + "3Symbols2": cfvo3, + "3TrafficLights1": cfvo3, + "3TrafficLights2": cfvo3, + "4Arrows": cfvo4, + "4ArrowsGray": cfvo4, + "4Rating": cfvo4, + "4RedToBlack": cfvo4, + "4TrafficLights": cfvo4, + "5Arrows": cfvo5, + "5ArrowsGray": cfvo5, + "5Quarters": cfvo5, + "5Rating": cfvo5, + } + cfRule, ok := presets[format.IconStyle] + if !ok { + return nil, nil + } + cfRule.Priority = p + 1 + cfRule.IconSet.IconSet = format.IconStyle + cfRule.IconSet.Reverse = format.ReverseIcons + cfRule.IconSet.ShowValue = boolPtr(!format.IconsOnly) + cfRule.Type = format.Type + return cfRule, nil +} + // getPaletteColor provides a function to convert the RBG color by given // string. func getPaletteColor(color string) string { diff --git a/styles_test.go b/styles_test.go index cd90a3c94c..864f14f297 100644 --- a/styles_test.go +++ b/styles_test.go @@ -176,11 +176,22 @@ func TestSetConditionalFormat(t *testing.T) { for _, ref := range []string{"A1:A2", "B1:B2"} { assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, condFmts)) } - // Test creating a conditional format with invalid extension list characters + f = NewFile() + // Test creating a conditional format with existing extension lists ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) - ws.(*xlsxWorksheet).ExtLst.Ext = "" + ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: ` + + `} + assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarSolid: true}})) + f = NewFile() + // Test creating a conditional format with invalid extension list characters + ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: ""} assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", condFmts), "XML syntax error on line 1: element closed by ") + // Test creating a conditional format with invalid icon set style + assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "iconSet", IconStyle: "unknown"}}), ErrParameterInvalid.Error()) } func TestGetConditionalFormats(t *testing.T) { @@ -194,8 +205,10 @@ func TestGetConditionalFormats(t *testing.T) { {{Type: "unique", Format: 1, Criteria: "="}}, {{Type: "3_color_scale", Criteria: "=", MinType: "num", MidType: "num", MaxType: "num", MinValue: "-10", MidValue: "50", MaxValue: "10", MinColor: "#FF0000", MidColor: "#00FF00", MaxColor: "#0000FF"}}, {{Type: "2_color_scale", Criteria: "=", MinType: "num", MaxType: "num", MinColor: "#FF0000", MaxColor: "#0000FF"}}, - {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarColor: "#638EC6", BarBorderColor: "#0000FF", BarOnly: true, BarSolid: true, StopIfTrue: true}}, + {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarOnly: true, BarSolid: true, StopIfTrue: true}}, + {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarDirection: "rightToLeft", BarOnly: true, BarSolid: true, StopIfTrue: true}}, {{Type: "formula", Format: 1, Criteria: "="}}, + {{Type: "iconSet", IconStyle: "3Arrows", ReverseIcons: true, IconsOnly: true}}, } { f := NewFile() err := f.SetConditionalFormat("Sheet1", "A1:A2", format) diff --git a/xmlDrawing.go b/xmlDrawing.go index 56ba3ebcd3..cc9585a629 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -107,6 +107,18 @@ const ( ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}" ) +// extensionURIPriority is the priority of URI in the extension lists. +var extensionURIPriority = []string{ + ExtURIConditionalFormattings, + ExtURIDataValidations, + ExtURISparklineGroups, + ExtURISlicerListX14, + ExtURIProtectedRanges, + ExtURIIgnoredErrors, + ExtURIWebExtensions, + ExtURITimelineRefs, +} + // Excel specifications and limits const ( MaxCellStyles = 65430 diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 07000bd4ae..09747339fe 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -777,8 +777,10 @@ type decodeX14DataBar struct { XNLName xml.Name `xml:"dataBar"` MaxLength int `xml:"maxLength,attr"` MinLength int `xml:"minLength,attr"` + Border bool `xml:"border,attr,omitempty"` Gradient bool `xml:"gradient,attr"` ShowValue bool `xml:"showValue,attr,omitempty"` + Direction string `xml:"direction,attr,omitempty"` Cfvo []*xlsxCfvo `xml:"cfvo"` BorderColor *xlsxColor `xml:"borderColor"` NegativeFillColor *xlsxColor `xml:"negativeFillColor"` @@ -801,7 +803,6 @@ type xlsxX14ConditionalFormatting struct { // xlsxX14CfRule directly maps the cfRule element. type xlsxX14CfRule struct { - XNLName xml.Name `xml:"x14:cfRule"` Type string `xml:"type,attr,omitempty"` ID string `xml:"id,attr,omitempty"` DataBar *xlsx14DataBar `xml:"x14:dataBar"` @@ -809,11 +810,12 @@ type xlsxX14CfRule struct { // xlsx14DataBar directly maps the dataBar element. type xlsx14DataBar struct { - XNLName xml.Name `xml:"x14:dataBar"` MaxLength int `xml:"maxLength,attr"` MinLength int `xml:"minLength,attr"` + Border bool `xml:"border,attr"` Gradient bool `xml:"gradient,attr"` ShowValue bool `xml:"showValue,attr,omitempty"` + Direction string `xml:"direction,attr,omitempty"` Cfvo []*xlsxCfvo `xml:"x14:cfvo"` BorderColor *xlsxColor `xml:"x14:borderColor"` NegativeFillColor *xlsxColor `xml:"x14:negativeFillColor"` @@ -942,8 +944,12 @@ type ConditionalFormatOptions struct { MaxLength string BarColor string BarBorderColor string + BarDirection string BarOnly bool BarSolid bool + IconStyle string + ReverseIcons bool + IconsOnly bool StopIfTrue bool } From 38f131728ba36c69f8397430137ef554c3b490a7 Mon Sep 17 00:00:00 2001 From: Josh Weston <10539811+Josh-Weston@users.noreply.github.com> Date: Sat, 11 Feb 2023 06:37:06 -0400 Subject: [PATCH 156/213] This closes #1463, add new functions `SetSheetDimension` and `GetSheetDimension` (#1467) --- sheet.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ sheet_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/sheet.go b/sheet.go index 68cbf50ea3..4f9b95771a 100644 --- a/sheet.go +++ b/sheet.go @@ -1883,3 +1883,52 @@ func makeContiguousColumns(ws *xlsxWorksheet, fromRow, toRow, colCount int) { fillColumns(rowData, colCount, fromRow) } } + +// SetSheetDimension provides the method to set or remove the used range of the +// worksheet by a given range reference. It specifies the row and column bounds +// of used cells in the worksheet. The range reference is set using the A1 +// reference style(e.g., "A1:D5"). Passing an empty range reference will remove +// the used range of the worksheet. +func (f *File) SetSheetDimension(sheet string, rangeRef string) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } + // Remove the dimension element if an empty string is provided + if rangeRef == "" { + ws.Dimension = nil + return nil + } + parts := len(strings.Split(rangeRef, ":")) + if parts == 1 { + _, _, err = CellNameToCoordinates(rangeRef) + if err == nil { + ws.Dimension = &xlsxDimension{Ref: strings.ToUpper(rangeRef)} + } + return err + } + if parts != 2 { + return ErrParameterInvalid + } + coordinates, err := rangeRefToCoordinates(rangeRef) + if err != nil { + return err + } + _ = sortCoordinates(coordinates) + ref, err := f.coordinatesToRangeRef(coordinates) + ws.Dimension = &xlsxDimension{Ref: ref} + return err +} + +// SetSheetDimension provides the method to get the used range of the worksheet. +func (f *File) GetSheetDimension(sheet string) (string, error) { + var ref string + ws, err := f.workSheetReader(sheet) + if err != nil { + return ref, err + } + if ws.Dimension != nil { + ref = ws.Dimension.Ref + } + return ref, err +} diff --git a/sheet_test.go b/sheet_test.go index 6d2e0f6d5c..fae4e596a3 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -644,3 +644,51 @@ func TestCheckSheetName(t *testing.T) { assert.EqualError(t, checkSheetName("'Sheet"), ErrSheetNameSingleQuote.Error()) assert.EqualError(t, checkSheetName("Sheet'"), ErrSheetNameSingleQuote.Error()) } + +func TestSheetDimension(t *testing.T) { + f := NewFile() + const sheetName = "Sheet1" + // Test get a new worksheet dimension + dimension, err := f.GetSheetDimension(sheetName) + assert.NoError(t, err) + assert.Equal(t, "A1", dimension) + // Test remove the worksheet dimension + assert.NoError(t, f.SetSheetDimension(sheetName, "")) + assert.NoError(t, err) + dimension, err = f.GetSheetDimension(sheetName) + assert.NoError(t, err) + assert.Equal(t, "", dimension) + // Test set the worksheet dimension + for _, excepted := range []string{"A1", "A1:D5", "A1:XFD1048576", "a1", "A1:d5"} { + err = f.SetSheetDimension(sheetName, excepted) + assert.NoError(t, err) + dimension, err := f.GetSheetDimension(sheetName) + assert.NoError(t, err) + assert.Equal(t, strings.ToUpper(excepted), dimension) + } + // Test set the worksheet dimension with invalid range reference or no exists worksheet + for _, c := range []struct { + sheetName string + rangeRef string + err string + }{ + {"Sheet1", "A-1", "cannot convert cell \"A-1\" to coordinates: invalid cell name \"A-1\""}, + {"Sheet1", "A1:B-1", "cannot convert cell \"B-1\" to coordinates: invalid cell name \"B-1\""}, + {"Sheet1", "A1:XFD1048577", "row number exceeds maximum limit"}, + {"Sheet1", "123", "cannot convert cell \"123\" to coordinates: invalid cell name \"123\""}, + {"Sheet1", "A:B", "cannot convert cell \"A\" to coordinates: invalid cell name \"A\""}, + {"Sheet1", ":B10", "cannot convert cell \"\" to coordinates: invalid cell name \"\""}, + {"Sheet1", "XFE1", "the column number must be greater than or equal to 1 and less than or equal to 16384"}, + {"Sheet1", "A1048577", "row number exceeds maximum limit"}, + {"Sheet1", "ZZZ", "cannot convert cell \"ZZZ\" to coordinates: invalid cell name \"ZZZ\""}, + {"SheetN", "A1", "sheet SheetN does not exist"}, + {"Sheet1", "A1:B3:D5", ErrParameterInvalid.Error()}, + } { + err = f.SetSheetDimension(c.sheetName, c.rangeRef) + assert.EqualError(t, err, c.err) + } + // Test get the worksheet dimension no exists worksheet + dimension, err = f.GetSheetDimension("SheetN") + assert.Empty(t, dimension) + assert.EqualError(t, err, "sheet SheetN does not exist") +} From 363fa940ac038f5c89aee0dbfa74fd9d1ce9e37e Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 13 Feb 2023 13:28:02 +0800 Subject: [PATCH 157/213] This closes #1468, checks the table name, and added a new error constant `ErrTableNameLength` - XML Structure field typo fixed - Update documentation for the `AddChart` function - Update unit test --- chart.go | 5 ++--- errors.go | 9 +++++++++ picture.go | 4 ++-- stream.go | 5 ++++- stream_test.go | 2 ++ table.go | 44 +++++++++++++++++++++++++++++++++++++++----- table_test.go | 19 +++++++++++++++++++ xmlWorksheet.go | 4 ++-- 8 files changed, 79 insertions(+), 13 deletions(-) diff --git a/chart.go b/chart.go index b983388f2d..839674ce7f 100644 --- a/chart.go +++ b/chart.go @@ -773,7 +773,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // The 'ShowVal' property is optional. The default value is false. // // Set the primary horizontal and vertical axis options by 'XAxis' and 'YAxis'. -// The properties of XAxis that can be set are: +// The properties of 'XAxis' that can be set are: // // None // MajorGridLines @@ -790,13 +790,12 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // MajorGridLines // MinorGridLines // MajorUnit -// TickLabelSkip // ReverseOrder // Maximum // Minimum // Font // -// none: Disable axes. +// None: Disable axes. // // MajorGridLines: Specifies major grid lines. // diff --git a/errors.go b/errors.go index 2a627d3de4..1357d0f8e7 100644 --- a/errors.go +++ b/errors.go @@ -40,6 +40,12 @@ func newInvalidExcelDateError(dateValue float64) error { return fmt.Errorf("invalid date value %f, negative values are not supported", dateValue) } +// newInvalidTableNameError defined the error message on receiving the invalid +// table name. +func newInvalidTableNameError(name string) error { + return fmt.Errorf("invalid table name %q", name) +} + // newUnsupportedChartType defined the error message on receiving the chart // type are unsupported. func newUnsupportedChartType(chartType string) error { @@ -230,6 +236,9 @@ var ( // ErrSheetNameLength defined the error message on receiving the sheet // name length exceeds the limit. ErrSheetNameLength = fmt.Errorf("the sheet name length exceeds the %d characters limit", MaxSheetNameLength) + // ErrTableNameLength defined the error message on receiving the table name + // length exceeds the limit. + ErrTableNameLength = fmt.Errorf("the table name length exceeds the %d characters limit", MaxFieldLength) // ErrCellStyles defined the error message on cell styles exceeds the limit. ErrCellStyles = fmt.Errorf("the cell styles exceeds the %d limit", MaxCellStyles) // ErrUnprotectWorkbook defined the error message on workbook has set no diff --git a/picture.go b/picture.go index f003852e27..54b03f2a04 100644 --- a/picture.go +++ b/picture.go @@ -110,14 +110,14 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { // } // } // -// The optional parameter "Autofit" specifies if you make image size auto-fits the +// The optional parameter "AutoFit" specifies if you make image size auto-fits the // cell, the default value of that is 'false'. // // The optional parameter "Hyperlink" specifies the hyperlink of the image. // // The optional parameter "HyperlinkType" defines two types of // hyperlink "External" for website or "Location" for moving to one of the -// cells in this workbook. When the "hyperlink_type" is "Location", +// cells in this workbook. When the "HyperlinkType" is "Location", // coordinates need to start with "#". // // The optional parameter "Positioning" defines two types of the position of an diff --git a/stream.go b/stream.go index 4be4defbf6..bafd7591b1 100644 --- a/stream.go +++ b/stream.go @@ -161,7 +161,10 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { // // See File.AddTable for details on the table format. func (sw *StreamWriter) AddTable(rangeRef string, opts *TableOptions) error { - options := parseTableOptions(opts) + options, err := parseTableOptions(opts) + if err != nil { + return err + } coordinates, err := rangeRefToCoordinates(rangeRef) if err != nil { return err diff --git a/stream_test.go b/stream_test.go index 195bdf099f..0e69055275 100644 --- a/stream_test.go +++ b/stream_test.go @@ -222,6 +222,8 @@ func TestStreamTable(t *testing.T) { // Test add table with illegal cell reference assert.EqualError(t, streamWriter.AddTable("A:B1", nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, streamWriter.AddTable("A1:B", nil), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + // Test add table with invalid table name + assert.EqualError(t, streamWriter.AddTable("A:B1", &TableOptions{Name: "1Table"}), newInvalidTableNameError("1Table").Error()) // Test add table with unsupported charset content types file.ContentTypes = nil file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) diff --git a/table.go b/table.go index d98fd7e61a..a00b0a2006 100644 --- a/table.go +++ b/table.go @@ -17,18 +17,24 @@ import ( "regexp" "strconv" "strings" + "unicode" + "unicode/utf8" ) // parseTableOptions provides a function to parse the format settings of the // table with default value. -func parseTableOptions(opts *TableOptions) *TableOptions { +func parseTableOptions(opts *TableOptions) (*TableOptions, error) { + var err error if opts == nil { - return &TableOptions{ShowRowStripes: boolPtr(true)} + return &TableOptions{ShowRowStripes: boolPtr(true)}, err } if opts.ShowRowStripes == nil { opts.ShowRowStripes = boolPtr(true) } - return opts + if err = checkTableName(opts.Name); err != nil { + return opts, err + } + return opts, err } // AddTable provides the method to add table in a worksheet by given worksheet @@ -54,7 +60,9 @@ func parseTableOptions(opts *TableOptions) *TableOptions { // header row data of the table before calling the AddTable function. Multiple // tables range reference that can't have an intersection. // -// Name: The name of the table, in the same worksheet name of the table should be unique +// Name: The name of the table, in the same worksheet name of the table should +// be unique, starts with a letter or underscore (_), doesn't include a +// space or character, and should be no more than 255 characters // // StyleName: The built-in table style names // @@ -62,7 +70,10 @@ func parseTableOptions(opts *TableOptions) *TableOptions { // TableStyleMedium1 - TableStyleMedium28 // TableStyleDark1 - TableStyleDark11 func (f *File) AddTable(sheet, rangeRef string, opts *TableOptions) error { - options := parseTableOptions(opts) + options, err := parseTableOptions(opts) + if err != nil { + return err + } // Coordinate conversion, convert C1:B3 to 2,0,1,2. coordinates, err := rangeRefToCoordinates(rangeRef) if err != nil { @@ -147,6 +158,29 @@ func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn, return tableColumns, nil } +// checkSheetName check whether there are illegal characters in the table name. +// Verify that the name: +// 1. Starts with a letter or underscore (_) +// 2. Doesn't include a space or character that isn't allowed +func checkTableName(name string) error { + if utf8.RuneCountInString(name) > MaxFieldLength { + return ErrTableNameLength + } + for i, c := range name { + if string(c) == "_" { + continue + } + if unicode.IsLetter(c) { + continue + } + if i > 0 && unicode.IsDigit(c) { + continue + } + return newInvalidTableNameError(name) + } + return nil +} + // addTable provides a function to add table by given worksheet name, // range reference and format set. func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *TableOptions) error { diff --git a/table_test.go b/table_test.go index 1e1afae372..33ce2e9033 100644 --- a/table_test.go +++ b/table_test.go @@ -3,6 +3,7 @@ package excelize import ( "fmt" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -37,6 +38,24 @@ func TestAddTable(t *testing.T) { f = NewFile() assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell reference [0, 0]") assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell reference [0, 0]") + // Test add table with invalid table name + for _, cases := range []struct { + name string + err error + }{ + {name: "1Table", err: newInvalidTableNameError("1Table")}, + {name: "-Table", err: newInvalidTableNameError("-Table")}, + {name: "'Table", err: newInvalidTableNameError("'Table")}, + {name: "Table 1", err: newInvalidTableNameError("Table 1")}, + {name: "A&B", err: newInvalidTableNameError("A&B")}, + {name: "_1Table'", err: newInvalidTableNameError("_1Table'")}, + {name: "\u0f5f\u0fb3\u0f0b\u0f21", err: newInvalidTableNameError("\u0f5f\u0fb3\u0f0b\u0f21")}, + {name: strings.Repeat("c", MaxFieldLength+1), err: ErrTableNameLength}, + } { + assert.EqualError(t, f.AddTable("Sheet1", "A1:B2", &TableOptions{ + Name: cases.name, + }), cases.err.Error()) + } } func TestSetTableHeader(t *testing.T) { diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 09747339fe..98727de8f7 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -766,7 +766,7 @@ type decodeX14ConditionalFormatting struct { // decodeX14CfRule directly maps the cfRule element. type decodeX14CfRule struct { - XNLName xml.Name `xml:"cfRule"` + XMLName xml.Name `xml:"cfRule"` Type string `xml:"type,attr,omitempty"` ID string `xml:"id,attr,omitempty"` DataBar *decodeX14DataBar `xml:"dataBar"` @@ -774,7 +774,7 @@ type decodeX14CfRule struct { // decodeX14DataBar directly maps the dataBar element. type decodeX14DataBar struct { - XNLName xml.Name `xml:"dataBar"` + XMLName xml.Name `xml:"dataBar"` MaxLength int `xml:"maxLength,attr"` MinLength int `xml:"minLength,attr"` Border bool `xml:"border,attr,omitempty"` From ad90cea78bc75f1407fdc3e7730fda26fc718040 Mon Sep 17 00:00:00 2001 From: jaby <97000+jaby@users.noreply.github.com> Date: Wed, 15 Feb 2023 14:38:11 +0100 Subject: [PATCH 158/213] This closes #1469, fix cell resolver caused incorrect calculation result (#1470) --- calc.go | 44 +++++++++++++++++++++++++++----------------- calc_test.go | 19 ++++++++++++++++++- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/calc.go b/calc.go index eff012fdfa..ddc07a74c3 100644 --- a/calc.go +++ b/calc.go @@ -197,8 +197,10 @@ var ( // calcContext defines the formula execution context. type calcContext struct { sync.Mutex - entry string - iterations map[string]uint + entry string + maxCalcIterations uint + iterations map[string]uint + iterationsCache map[string]formulaArg } // cellRef defines the structure of a cell reference. @@ -774,8 +776,10 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string token formulaArg ) if token, err = f.calcCellValue(&calcContext{ - entry: fmt.Sprintf("%s!%s", sheet, cell), - iterations: make(map[string]uint), + entry: fmt.Sprintf("%s!%s", sheet, cell), + maxCalcIterations: getOptions(opts...).MaxCalcIterations, + iterations: make(map[string]uint), + iterationsCache: make(map[string]formulaArg), }, sheet, cell); err != nil { return } @@ -1527,17 +1531,6 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e value string err error ) - ref := fmt.Sprintf("%s!%s", sheet, cell) - if formula, _ := f.GetCellFormula(sheet, cell); len(formula) != 0 { - ctx.Lock() - if ctx.entry != ref && ctx.iterations[ref] <= f.options.MaxCalcIterations { - ctx.iterations[ref]++ - ctx.Unlock() - arg, _ = f.calcCellValue(ctx, sheet, cell) - return arg, nil - } - ctx.Unlock() - } if value, err = f.GetCellValue(sheet, cell, Options{RawCellValue: true}); err != nil { return arg, err } @@ -1551,8 +1544,25 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e return newEmptyFormulaArg(), err } return arg.ToNumber(), err - default: + case CellTypeInlineString, CellTypeSharedString: return arg, err + case CellTypeFormula: + ref := fmt.Sprintf("%s!%s", sheet, cell) + if ctx.entry != ref { + ctx.Lock() + if ctx.iterations[ref] <= ctx.maxCalcIterations { + ctx.iterations[ref]++ + ctx.Unlock() + arg, _ = f.calcCellValue(ctx, sheet, cell) + ctx.iterationsCache[ref] = arg + return arg, nil + } + ctx.Unlock() + return ctx.iterationsCache[ref], nil + } + fallthrough + default: + return newEmptyFormulaArg(), err } } @@ -7746,7 +7756,7 @@ func (fn *formulaFuncs) COUNTBLANK(argsList *list.List) formulaArg { } var count float64 for _, cell := range argsList.Front().Value.(formulaArg).ToList() { - if cell.Value() == "" { + if cell.Type == ArgEmpty { count++ } } diff --git a/calc_test.go b/calc_test.go index 5e87763ea7..c740e6b454 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1023,7 +1023,7 @@ func TestCalcCellValue(t *testing.T) { "=COUNTBLANK(MUNIT(1))": "0", "=COUNTBLANK(1)": "0", "=COUNTBLANK(B1:C1)": "1", - "=COUNTBLANK(C1)": "1", + "=COUNTBLANK(C1)": "0", // COUNTIF "=COUNTIF(D1:D9,\"Jan\")": "4", "=COUNTIF(D1:D9,\"<>Jan\")": "5", @@ -5871,3 +5871,20 @@ func TestCalcColRowQRDecomposition(t *testing.T) { assert.False(t, calcRowQRDecomposition([][]float64{{0, 0}, {0, 0}}, []float64{0, 0}, 1, 0)) assert.False(t, calcColQRDecomposition([][]float64{{0, 0}, {0, 0}}, []float64{0, 0}, 1, 0)) } + +func TestCalcCellResolver(t *testing.T) { + f := NewFile() + // Test reference a cell multiple times in a formula + assert.NoError(t, f.SetCellValue("Sheet1", "A1", "VALUE1")) + assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=A1")) + for formula, expected := range map[string]string{ + "=CONCATENATE(A1,\"_\",A1)": "VALUE1_VALUE1", + "=CONCATENATE(A1,\"_\",A2)": "VALUE1_VALUE1", + "=CONCATENATE(A2,\"_\",A2)": "VALUE1_VALUE1", + } { + assert.NoError(t, f.SetCellFormula("Sheet1", "A3", formula)) + result, err := f.CalcCellValue("Sheet1", "A3") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } +} From c2d6707a850bdc7dbb32f68481b4b266b9cf7367 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 17 Feb 2023 20:03:46 +0800 Subject: [PATCH 159/213] This closes #1474, support set the format for the data series fill (solid fill) - Breaking changes: remove the `Color` field in the `ChartLine` structure - This support set the bubble size in a data series - Unit test update and correct the docs of the function `GetSheetDimension` --- chart.go | 8 ++++++-- chart_test.go | 26 +++++++++++++++++--------- drawing.go | 52 +++++++++++++++++++++++++++++++++++++-------------- sheet.go | 2 +- xmlChart.go | 3 ++- 5 files changed, 64 insertions(+), 27 deletions(-) diff --git a/chart.go b/chart.go index 839674ce7f..a58eac4f82 100644 --- a/chart.go +++ b/chart.go @@ -657,7 +657,9 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // // Name // Categories +// Sizes // Values +// Fill // Line // Marker // @@ -670,16 +672,18 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // the same as the X axis. In most chart types the 'Categories' property is // optional and the chart will just assume a sequential series from 1..n. // +// Sizes: This sets the bubble size in a data series. +// // Values: This is the most important property of a series and is the only // mandatory option for every chart object. This option links the chart with // the worksheet data that it displays. // +// Fill: This set the format for the data series fill. +// // Line: This sets the line format of the line chart. The 'Line' property is // optional and if it isn't supplied it will default style. The options that // can be set are width and color. The range of width is 0.25pt - 999pt. If the // value of width is outside the range, the default width of the line is 2pt. -// The value for color should be represented in hex format -// (e.g., #000000 - #FFFFFF) // // Marker: This sets the marker of the line chart and scatter chart. The range // of optional field 'Size' is 2-72 (default value is 5). The enumeration value diff --git a/chart_test.go b/chart_test.go index 2c740ef324..cc78944e87 100644 --- a/chart_test.go +++ b/chart_test.go @@ -153,7 +153,9 @@ func TestAddChart(t *testing.T) { {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"}, } series2 := []ChartSeries{ - {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", Marker: ChartMarker{Symbol: "none", Size: 10}, Line: ChartLine{Color: "#000000"}}, + {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", + Fill: Fill{Type: "pattern", Color: []string{"000000"}, Pattern: 1}, + Marker: ChartMarker{Symbol: "none", Size: 10}}, {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31"}, {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32"}, {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33"}, @@ -163,6 +165,16 @@ func TestAddChart(t *testing.T) { {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Line: ChartLine{Width: 0.25}}, } series3 := []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}} + series4 := []ChartSeries{ + {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", Sizes: "Sheet1!$B$30:$D$30"}, + {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31", Sizes: "Sheet1!$B$31:$D$31"}, + {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32", Sizes: "Sheet1!$B$32:$D$32"}, + {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33", Sizes: "Sheet1!$B$33:$D$33"}, + {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34", Sizes: "Sheet1!$B$34:$D$34"}, + {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35", Sizes: "Sheet1!$B$35:$D$35"}, + {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36", Sizes: "Sheet1!$B$36:$D$36"}, + {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Sizes: "Sheet1!$B$37:$D$37"}, + } format := GraphicOptions{ ScaleX: defaultPictureScale, ScaleY: defaultPictureScale, @@ -242,8 +254,8 @@ func TestAddChart(t *testing.T) { {sheetName: "Sheet2", cell: "AV32", opts: &Chart{Type: "contour", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "BD1", opts: &Chart{Type: "wireframeContour", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Wireframe Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, // bubble chart - {sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: "bubble", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: "bubble3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, + {sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: "bubble", Series: series4, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: "bubble3D", Series: series4, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, // pie of pie chart {sheetName: "Sheet2", cell: "BD48", opts: &Chart{Type: "pieOfPie", Series: series3, Format: format, Legend: legend, Title: ChartTitle{Name: "Pie of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, // bar of pie chart @@ -256,18 +268,14 @@ func TestAddChart(t *testing.T) { assert.NoError(t, err) clusteredColumnCombo := [][]string{ {"A1", "line", "Clustered Column - Line Chart"}, - {"I1", "bubble", "Clustered Column - Bubble Chart"}, - {"Q1", "bubble3D", "Clustered Column - Bubble 3D Chart"}, - {"Y1", "doughnut", "Clustered Column - Doughnut Chart"}, + {"I1", "doughnut", "Clustered Column - Doughnut Chart"}, } for _, props := range clusteredColumnCombo { assert.NoError(t, f.AddChart("Combo Charts", props[0], &Chart{Type: "col", Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[2]}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1], Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) } stackedAreaCombo := map[string][]string{ "A16": {"line", "Stacked Area - Line Chart"}, - "I16": {"bubble", "Stacked Area - Bubble Chart"}, - "Q16": {"bubble3D", "Stacked Area - Bubble 3D Chart"}, - "Y16": {"doughnut", "Stacked Area - Doughnut Chart"}, + "I16": {"doughnut", "Stacked Area - Doughnut Chart"}, } for axis, props := range stackedAreaCombo { assert.NoError(t, f.AddChart("Combo Charts", axis, &Chart{Type: "areaStacked", Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[1]}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[0], Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) diff --git a/drawing.go b/drawing.go index 88aaab83fc..7ce4b9e3af 100644 --- a/drawing.go +++ b/drawing.go @@ -234,8 +234,8 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { WireframeSurface3D: f.drawSurface3DChart, Contour: f.drawSurfaceChart, WireframeContour: f.drawSurfaceChart, - Bubble: f.drawBaseChart, - Bubble3D: f.drawBaseChart, + Bubble: f.drawBubbleChart, + Bubble3D: f.drawBubbleChart, } if opts.Legend.Position == "none" { xlsxChartSpace.Chart.Legend = nil @@ -270,7 +270,7 @@ func (f *File) drawBaseChart(opts *Chart) *cPlotArea { Val: stringPtr("col"), }, Grouping: &attrValString{ - Val: stringPtr("clustered"), + Val: stringPtr(plotAreaChartGrouping[opts.Type]), }, VaryColors: &attrValBool{ Val: opts.VaryColors, @@ -288,9 +288,6 @@ func (f *File) drawBaseChart(opts *Chart) *cPlotArea { if *c.BarDir.Val, ok = plotAreaChartBarDir[opts.Type]; !ok { c.BarDir = nil } - if *c.Grouping.Val, ok = plotAreaChartGrouping[opts.Type]; !ok { - c.Grouping = nil - } if *c.Overlap.Val, ok = plotAreaChartOverlap[opts.Type]; !ok { c.Overlap = nil } @@ -726,6 +723,26 @@ func (f *File) drawSurfaceChart(opts *Chart) *cPlotArea { return plotArea } +// drawBubbleChart provides a function to draw the c:bubbleChart element by +// given format sets. +func (f *File) drawBubbleChart(opts *Chart) *cPlotArea { + plotArea := &cPlotArea{ + BubbleChart: &cCharts{ + VaryColors: &attrValBool{ + Val: opts.VaryColors, + }, + Ser: f.drawChartSeries(opts), + DLbls: f.drawChartDLbls(opts), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + }, + }, + ValAx: []*cAxs{f.drawPlotAreaCatAx(opts)[0], f.drawPlotAreaValAx(opts)[0]}, + } + return plotArea +} + // drawChartShape provides a function to draw the c:shape element by given // format sets. func (f *File) drawChartShape(opts *Chart) *attrValString { @@ -794,13 +811,13 @@ func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr { var srgbClr *attrValString var schemeClr *aSchemeClr - if color := stringPtr(opts.Series[i].Line.Color); *color != "" { - *color = strings.TrimPrefix(*color, "#") - srgbClr = &attrValString{Val: color} + if color := opts.Series[i].Fill.Color; len(color) == 1 { + srgbClr = &attrValString{Val: stringPtr(strings.TrimPrefix(color[0], "#"))} } else { schemeClr = &aSchemeClr{Val: "accent" + strconv.Itoa((opts.order+i)%6+1)} } + spPr := &cSpPr{SolidFill: &aSolidFill{SchemeClr: schemeClr, SrgbClr: srgbClr}} spPrScatter := &cSpPr{ Ln: &aLn{ W: 25400, @@ -817,8 +834,15 @@ func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr { }, }, } - chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter} - return chartSeriesSpPr[opts.Type] + if chartSeriesSpPr, ok := map[string]*cSpPr{ + Line: spPrLine, Scatter: spPrScatter, + }[opts.Type]; ok { + return chartSeriesSpPr + } + if srgbClr != nil { + return spPr + } + return nil } // drawChartSeriesDPt provides a function to draw the c:dPt element by given @@ -923,7 +947,7 @@ func (f *File) drawChartSeriesXVal(v ChartSeries, opts *Chart) *cCat { F: v.Categories, }, } - chartSeriesXVal := map[string]*cCat{Scatter: cat} + chartSeriesXVal := map[string]*cCat{Scatter: cat, Bubble: cat, Bubble3D: cat} return chartSeriesXVal[opts.Type] } @@ -942,12 +966,12 @@ func (f *File) drawChartSeriesYVal(v ChartSeries, opts *Chart) *cVal { // drawCharSeriesBubbleSize provides a function to draw the c:bubbleSize // element by given chart series and format sets. func (f *File) drawCharSeriesBubbleSize(v ChartSeries, opts *Chart) *cVal { - if _, ok := map[string]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok { + if _, ok := map[string]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok || v.Sizes == "" { return nil } return &cVal{ NumRef: &cNumRef{ - F: v.Values, + F: v.Sizes, }, } } diff --git a/sheet.go b/sheet.go index 4f9b95771a..0bc6815452 100644 --- a/sheet.go +++ b/sheet.go @@ -1920,7 +1920,7 @@ func (f *File) SetSheetDimension(sheet string, rangeRef string) error { return err } -// SetSheetDimension provides the method to get the used range of the worksheet. +// GetSheetDimension provides the method to get the used range of the worksheet. func (f *File) GetSheetDimension(sheet string) (string, error) { var ref string ws, err := f.workSheetReader(sheet) diff --git a/xmlChart.go b/xmlChart.go index 9818ca1824..6688ed16e9 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -579,7 +579,6 @@ type ChartMarker struct { // ChartLine directly maps the format settings of the chart line. type ChartLine struct { - Color string Smooth bool Width float64 } @@ -588,7 +587,9 @@ type ChartLine struct { type ChartSeries struct { Name string Categories string + Sizes string Values string + Fill Fill Line ChartLine Marker ChartMarker } From 21ec143778333073436daf85e92fe5e2f1c3e620 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Feb 2023 14:28:47 +0800 Subject: [PATCH 160/213] Update dependencies package golang.org/x/net from 0.5.0 to 0.7.0 (#1475) --- go.mod | 4 ++-- go.sum | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index b6c63e8b43..d5b408bd38 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 golang.org/x/crypto v0.5.0 golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 - golang.org/x/net v0.5.0 - golang.org/x/text v0.6.0 + golang.org/x/net v0.7.0 + golang.org/x/text v0.7.0 ) require github.com/richardlehane/msoleps v1.0.3 // indirect diff --git a/go.sum b/go.sum index 7e2848d986..7f0afed2a7 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,9 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -40,14 +41,17 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/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/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From cb0c1b012b55be6feccb99e66b7d9cae2c45e72f Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 19 Feb 2023 00:18:11 +0800 Subject: [PATCH 161/213] Support specifies the values in second plot for the bar/pie of pie chart - Upgrade dependencies package golang.org/x/image to 0.5.0 - Update unit tests --- chart.go | 4 ++++ chart_test.go | 13 +++++++------ drawing.go | 10 ++++++++++ go.mod | 2 +- go.sum | 4 ++-- xmlChart.go | 14 ++++++++------ 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/chart.go b/chart.go index a58eac4f82..a9b3aaae67 100644 --- a/chart.go +++ b/chart.go @@ -751,6 +751,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Set the position of the chart plot area by PlotArea. The properties that can // be set are: // +// SecondPlotValues // ShowBubbleSize // ShowCatName // ShowLeaderLines @@ -758,6 +759,9 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // ShowSerName // ShowVal // +// SecondPlotValues: Specifies the values in second plot for the 'pieOfPie' and +// 'barOfPie' chart. +// // ShowBubbleSize: Specifies the bubble size shall be shown in a data label. The // 'ShowBubbleSize' property is optional. The default value is false. // diff --git a/chart_test.go b/chart_test.go index cc78944e87..9a8660ce19 100644 --- a/chart_test.go +++ b/chart_test.go @@ -186,12 +186,13 @@ func TestAddChart(t *testing.T) { } legend := ChartLegend{Position: "left", ShowLegendKey: false} plotArea := ChartPlotArea{ - ShowBubbleSize: true, - ShowCatName: true, - ShowLeaderLines: false, - ShowPercent: true, - ShowSerName: true, - ShowVal: true, + SecondPlotValues: 3, + ShowBubbleSize: true, + ShowCatName: true, + ShowLeaderLines: false, + ShowPercent: true, + ShowSerName: true, + ShowVal: true, } for _, c := range []struct { sheetName, cell string diff --git a/drawing.go b/drawing.go index 7ce4b9e3af..93684a3ff2 100644 --- a/drawing.go +++ b/drawing.go @@ -602,6 +602,10 @@ func (f *File) drawPie3DChart(opts *Chart) *cPlotArea { // drawPieOfPieChart provides a function to draw the c:plotArea element for // pie chart by given format sets. func (f *File) drawPieOfPieChart(opts *Chart) *cPlotArea { + var splitPos *attrValInt + if opts.PlotArea.SecondPlotValues > 0 { + splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)} + } return &cPlotArea{ OfPieChart: &cCharts{ OfPieType: &attrValString{ @@ -611,6 +615,7 @@ func (f *File) drawPieOfPieChart(opts *Chart) *cPlotArea { Val: opts.VaryColors, }, Ser: f.drawChartSeries(opts), + SplitPos: splitPos, SerLines: &attrValString{}, }, } @@ -619,6 +624,10 @@ func (f *File) drawPieOfPieChart(opts *Chart) *cPlotArea { // drawBarOfPieChart provides a function to draw the c:plotArea element for // pie chart by given format sets. func (f *File) drawBarOfPieChart(opts *Chart) *cPlotArea { + var splitPos *attrValInt + if opts.PlotArea.SecondPlotValues > 0 { + splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)} + } return &cPlotArea{ OfPieChart: &cCharts{ OfPieType: &attrValString{ @@ -627,6 +636,7 @@ func (f *File) drawBarOfPieChart(opts *Chart) *cPlotArea { VaryColors: &attrValBool{ Val: opts.VaryColors, }, + SplitPos: splitPos, Ser: f.drawChartSeries(opts), SerLines: &attrValString{}, }, diff --git a/go.mod b/go.mod index d5b408bd38..a4a0a74daf 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 golang.org/x/crypto v0.5.0 - golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 + golang.org/x/image v0.5.0 golang.org/x/net v0.7.0 golang.org/x/text v0.7.0 ) diff --git a/go.sum b/go.sum index 7f0afed2a7..7e2b95af21 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= -golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= +golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= diff --git a/xmlChart.go b/xmlChart.go index 6688ed16e9..6c17ab8760 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -333,6 +333,7 @@ type cCharts struct { VaryColors *attrValBool `xml:"varyColors"` Wireframe *attrValBool `xml:"wireframe"` Ser *[]cSer `xml:"ser"` + SplitPos *attrValInt `xml:"splitPos"` SerLines *attrValString `xml:"serLines"` DLbls *cDLbls `xml:"dLbls"` Shape *attrValString `xml:"shape"` @@ -540,12 +541,13 @@ type ChartDimension struct { // ChartPlotArea directly maps the format settings of the plot area. type ChartPlotArea struct { - ShowBubbleSize bool - ShowCatName bool - ShowLeaderLines bool - ShowPercent bool - ShowSerName bool - ShowVal bool + SecondPlotValues int + ShowBubbleSize bool + ShowCatName bool + ShowLeaderLines bool + ShowPercent bool + ShowSerName bool + ShowVal bool } // Chart directly maps the format settings of the chart. From 983cd76485740240dfc5e8789f582b37363b9444 Mon Sep 17 00:00:00 2001 From: Shugo Kawamura Date: Mon, 20 Feb 2023 14:59:05 +0900 Subject: [PATCH 162/213] This closes #1476, support double-byte chars for formula functions LEFT,RIGHT, LEN and MID (#1477) --- calc.go | 15 +++++++-------- calc_test.go | 10 ++++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/calc.go b/calc.go index ddc07a74c3..819bb0a45f 100644 --- a/calc.go +++ b/calc.go @@ -29,6 +29,7 @@ import ( "sync" "time" "unicode" + "unicode/utf8" "unsafe" "github.com/xuri/efp" @@ -13446,11 +13447,11 @@ func (fn *formulaFuncs) leftRight(name string, argsList *list.List) formulaArg { } numChars = int(numArg.Number) } - if len(text) > numChars { + if utf8.RuneCountInString(text) > numChars { if name == "LEFT" || name == "LEFTB" { - return newStringFormulaArg(text[:numChars]) + return newStringFormulaArg(string([]rune(text)[:numChars])) } - return newStringFormulaArg(text[len(text)-numChars:]) + return newStringFormulaArg(string([]rune(text)[utf8.RuneCountInString(text)-numChars:])) } return newStringFormulaArg(text) } @@ -13463,7 +13464,7 @@ func (fn *formulaFuncs) LEN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LEN requires 1 string argument") } - return newStringFormulaArg(strconv.Itoa(len(argsList.Front().Value.(formulaArg).String))) + return newStringFormulaArg(strconv.Itoa(utf8.RuneCountInString(argsList.Front().Value.(formulaArg).String))) } // LENB returns the number of bytes used to represent the characters in a text @@ -13510,9 +13511,7 @@ func (fn *formulaFuncs) MIDB(argsList *list.List) formulaArg { return fn.mid("MIDB", argsList) } -// mid is an implementation of the formula functions MID and MIDB. TODO: -// support DBCS include Japanese, Chinese (Simplified), Chinese -// (Traditional), and Korean. +// mid is an implementation of the formula functions MID and MIDB. func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 arguments", name)) @@ -13529,7 +13528,7 @@ func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg { if startNum < 0 { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - textLen := len(text) + textLen := utf8.RuneCountInString(text) if startNum > textLen { return newStringFormulaArg("") } diff --git a/calc_test.go b/calc_test.go index c740e6b454..f6f8b5382a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1697,6 +1697,11 @@ func TestCalcCellValue(t *testing.T) { "=LEFT(\"Original Text\",0)": "", "=LEFT(\"Original Text\",13)": "Original Text", "=LEFT(\"Original Text\",20)": "Original Text", + "=LEFT(\"オリジナルテキスト\")": "オ", + "=LEFT(\"オリジナルテキスト\",2)": "オリ", + "=LEFT(\"オリジナルテキスト\",5)": "オリジナル", + "=LEFT(\"オリジナルテキスト\",7)": "オリジナルテキ", + "=LEFT(\"オリジナルテキスト\",20)": "オリジナルテキスト", // LEFTB "=LEFTB(\"Original Text\")": "O", "=LEFTB(\"Original Text\",4)": "Orig", @@ -1751,6 +1756,11 @@ func TestCalcCellValue(t *testing.T) { "=RIGHT(\"Original Text\",0)": "", "=RIGHT(\"Original Text\",13)": "Original Text", "=RIGHT(\"Original Text\",20)": "Original Text", + "=RIGHT(\"オリジナルテキスト\")": "ト", + "=RIGHT(\"オリジナルテキスト\",2)": "スト", + "=RIGHT(\"オリジナルテキスト\",4)": "テキスト", + "=RIGHT(\"オリジナルテキスト\",7)": "ジナルテキスト", + "=RIGHT(\"オリジナルテキスト\",20)": "オリジナルテキスト", // RIGHTB "=RIGHTB(\"Original Text\")": "t", "=RIGHTB(\"Original Text\",4)": "Text", From f143dd5c3499bc508d57f826506d15a2a811ad3a Mon Sep 17 00:00:00 2001 From: Shugo Kawamura Date: Tue, 21 Feb 2023 01:17:35 +0900 Subject: [PATCH 163/213] Support double-byte chars for formula functions LENB, RIGHTB and MIDB (#1478) --- calc.go | 46 +++++++++++++++++++++++++++++++++++++++------- calc_test.go | 14 ++++++++++---- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/calc.go b/calc.go index 819bb0a45f..f8d461648c 100644 --- a/calc.go +++ b/calc.go @@ -13426,9 +13426,7 @@ func (fn *formulaFuncs) LEFTB(argsList *list.List) formulaArg { return fn.leftRight("LEFTB", argsList) } -// leftRight is an implementation of the formula functions LEFT, LEFTB, RIGHT, -// RIGHTB. TODO: support DBCS include Japanese, Chinese (Simplified), Chinese -// (Traditional), and Korean. +// leftRight is an implementation of the formula functions LEFT, LEFTB, RIGHT, RIGHTB. func (fn *formulaFuncs) leftRight(name string, argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name)) @@ -13447,10 +13445,22 @@ func (fn *formulaFuncs) leftRight(name string, argsList *list.List) formulaArg { } numChars = int(numArg.Number) } + if name == "LEFTB" || name == "RIGHTB" { + if len(text) > numChars { + if name == "LEFTB" { + return newStringFormulaArg(text[:numChars]) + } + // RIGHTB + return newStringFormulaArg(text[len(text)-numChars:]) + } + return newStringFormulaArg(text) + } + // LEFT/RIGHT if utf8.RuneCountInString(text) > numChars { - if name == "LEFT" || name == "LEFTB" { + if name == "LEFT" { return newStringFormulaArg(string([]rune(text)[:numChars])) } + // RIGHT return newStringFormulaArg(string([]rune(text)[utf8.RuneCountInString(text)-numChars:])) } return newStringFormulaArg(text) @@ -13480,7 +13490,16 @@ func (fn *formulaFuncs) LENB(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LENB requires 1 string argument") } - return newStringFormulaArg(strconv.Itoa(len(argsList.Front().Value.(formulaArg).String))) + bytes := 0 + for _, r := range []rune(argsList.Front().Value.(formulaArg).String) { + b := utf8.RuneLen(r) + if b == 1 { + bytes++ + } else if b > 1 { + bytes += 2 + } + } + return newStringFormulaArg(strconv.Itoa(bytes)) } // LOWER converts all characters in a supplied text string to lower case. The @@ -13528,6 +13547,19 @@ func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg { if startNum < 0 { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } + if name == "MIDB" { + textLen := len(text) + if startNum > textLen { + return newStringFormulaArg("") + } + startNum-- + endNum := startNum + int(numCharsArg.Number) + if endNum > textLen+1 { + return newStringFormulaArg(text[startNum:]) + } + return newStringFormulaArg(text[startNum:endNum]) + } + // MID textLen := utf8.RuneCountInString(text) if startNum > textLen { return newStringFormulaArg("") @@ -13535,9 +13567,9 @@ func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg { startNum-- endNum := startNum + int(numCharsArg.Number) if endNum > textLen+1 { - return newStringFormulaArg(text[startNum:]) + return newStringFormulaArg(string([]rune(text)[startNum:])) } - return newStringFormulaArg(text[startNum:endNum]) + return newStringFormulaArg(string([]rune(text)[startNum:endNum])) } // PROPER converts all characters in a supplied text string to proper case diff --git a/calc_test.go b/calc_test.go index f6f8b5382a..8c0ac2e369 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1709,11 +1709,15 @@ func TestCalcCellValue(t *testing.T) { "=LEFTB(\"Original Text\",13)": "Original Text", "=LEFTB(\"Original Text\",20)": "Original Text", // LEN - "=LEN(\"\")": "0", - "=LEN(D1)": "5", + "=LEN(\"\")": "0", + "=LEN(D1)": "5", + "=LEN(\"テキスト\")": "4", + "=LEN(\"オリジナルテキスト\")": "9", // LENB - "=LENB(\"\")": "0", - "=LENB(D1)": "5", + "=LENB(\"\")": "0", + "=LENB(D1)": "5", + "=LENB(\"テキスト\")": "8", + "=LENB(\"オリジナルテキスト\")": "18", // LOWER "=LOWER(\"test\")": "test", "=LOWER(\"TEST\")": "test", @@ -1725,6 +1729,8 @@ func TestCalcCellValue(t *testing.T) { "=MID(\"255 years\",3,1)": "5", "=MID(\"text\",3,6)": "xt", "=MID(\"text\",6,0)": "", + "=MID(\"オリジナルテキスト\",6,4)": "テキスト", + "=MID(\"オリジナルテキスト\",3,5)": "ジナルテキ", // MIDB "=MIDB(\"Original Text\",7,1)": "a", "=MIDB(\"Original Text\",4,7)": "ginal T", From 94e86dca31476ec79f0643ae9645893507ec56f3 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 22 Feb 2023 22:46:36 +0800 Subject: [PATCH 164/213] This renamed conditional format type `iconSet` to `icon_set` - Remove Minimum, Maximum, MinLength, and MaxLength fields from the type `ConditionalFormatOptions` - Update unit tests and format code --- calc.go | 6 ++---- chart_test.go | 6 ++++-- excelize_test.go | 4 ++-- styles.go | 40 ++++++++++++++++++++-------------------- styles_test.go | 6 +++--- xmlWorksheet.go | 4 ---- 6 files changed, 31 insertions(+), 35 deletions(-) diff --git a/calc.go b/calc.go index f8d461648c..70b5409537 100644 --- a/calc.go +++ b/calc.go @@ -13426,7 +13426,8 @@ func (fn *formulaFuncs) LEFTB(argsList *list.List) formulaArg { return fn.leftRight("LEFTB", argsList) } -// leftRight is an implementation of the formula functions LEFT, LEFTB, RIGHT, RIGHTB. +// leftRight is an implementation of the formula functions LEFT, LEFTB, RIGHT, +// RIGHTB. func (fn *formulaFuncs) leftRight(name string, argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name)) @@ -13483,9 +13484,6 @@ func (fn *formulaFuncs) LEN(argsList *list.List) formulaArg { // 1 byte per character. The syntax of the function is: // // LENB(text) -// -// TODO: the languages that support DBCS include Japanese, Chinese -// (Simplified), Chinese (Traditional), and Korean. func (fn *formulaFuncs) LENB(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LENB requires 1 string argument") diff --git a/chart_test.go b/chart_test.go index 9a8660ce19..14adb062a6 100644 --- a/chart_test.go +++ b/chart_test.go @@ -153,9 +153,11 @@ func TestAddChart(t *testing.T) { {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"}, } series2 := []ChartSeries{ - {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", + { + Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", Fill: Fill{Type: "pattern", Color: []string{"000000"}, Pattern: 1}, - Marker: ChartMarker{Symbol: "none", Size: 10}}, + Marker: ChartMarker{Symbol: "none", Size: 10}, + }, {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31"}, {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32"}, {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33"}, diff --git a/excelize_test.go b/excelize_test.go index 7e19c5b802..ea3a41bfbe 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1052,8 +1052,8 @@ func TestConditionalFormat(t *testing.T) { Type: "cell", Criteria: "between", Format: format1, - Minimum: "6", - Maximum: "8", + MinValue: "6", + MaxValue: "8", }, }, )) diff --git a/styles.go b/styles.go index 78083f2b07..7483d30fe4 100644 --- a/styles.go +++ b/styles.go @@ -808,7 +808,7 @@ var validType = map[string]string{ "3_color_scale": "3_color_scale", "data_bar": "dataBar", "formula": "expression", - "iconSet": "iconSet", + "icon_set": "iconSet", } // criteriaType defined the list of valid criteria types. @@ -2843,12 +2843,12 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // ---------------+------------------------------------ // cell | Criteria // | Value -// | Minimum -// | Maximum +// | MinValue +// | MaxValue // date | Criteria // | Value -// | Minimum -// | Maximum +// | MinValue +// | MaxValue // time_period | Criteria // text | Criteria // | Value @@ -2887,7 +2887,7 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // | BarDirection // | BarOnly // | BarSolid -// iconSet | IconStyle +// icon_set | IconStyle // | ReverseIcons // | IconsOnly // formula | Criteria @@ -2999,7 +2999,7 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // }, // ) // -// type: Minimum - The 'Minimum' parameter is used to set the lower limiting +// type: MinValue - The 'MinValue' parameter is used to set the lower limiting // value when the criteria is either "between" or "not between". // // // Highlight cells rules: between... @@ -3009,13 +3009,13 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // Type: "cell", // Criteria: "between", // Format: format, -// Minimum: "6", -// Maximum: "8", +// MinValue: 6", +// MaxValue: 8", // }, // }, // ) // -// type: Maximum - The 'Maximum' parameter is used to set the upper limiting +// type: MaxValue - The 'MaxValue' parameter is used to set the upper limiting // value when the criteria is either "between" or "not between". See the // previous example. // @@ -3361,7 +3361,7 @@ func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error { func extractCondFmtCellIs(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "cell", Criteria: operatorType[c.Operator], Format: *c.DxfID} if len(c.Formula) == 2 { - format.Minimum, format.Maximum = c.Formula[0], c.Formula[1] + format.MinValue, format.MaxValue = c.Formula[0], c.Formula[1] return format } format.Value = c.Formula[0] @@ -3514,7 +3514,7 @@ func extractCondFmtExp(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptio // extractCondFmtIconSet provides a function to extract conditional format // settings for icon sets by given conditional formatting rule. func extractCondFmtIconSet(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{Type: "iconSet"} + format := ConditionalFormatOptions{Type: "icon_set"} if c.IconSet != nil { if c.IconSet.ShowValue != nil { format.IconsOnly = !*c.IconSet.ShowValue @@ -3582,11 +3582,11 @@ func drawCondFmtCellIs(p int, ct, GUID string, format *ConditionalFormatOptions) StopIfTrue: format.StopIfTrue, Type: validType[format.Type], Operator: ct, - DxfID: &format.Format, + DxfID: intPtr(format.Format), } // "between" and "not between" criteria require 2 values. if ct == "between" || ct == "notBetween" { - c.Formula = append(c.Formula, []string{format.Minimum, format.Maximum}...) + c.Formula = append(c.Formula, []string{format.MinValue, format.MaxValue}...) } if idx := inStrSlice([]string{"equal", "notEqual", "greaterThan", "lessThan", "greaterThanOrEqual", "lessThanOrEqual", "containsText", "notContains", "beginsWith", "endsWith"}, ct, true); idx != -1 { c.Formula = append(c.Formula, format.Value) @@ -3604,7 +3604,7 @@ func drawCondFmtTop10(p int, ct, GUID string, format *ConditionalFormatOptions) Bottom: format.Type == "bottom", Type: validType[format.Type], Rank: 10, - DxfID: &format.Format, + DxfID: intPtr(format.Format), Percent: format.Percent, } if rank, err := strconv.Atoi(format.Value); err == nil { @@ -3621,8 +3621,8 @@ func drawCondFmtAboveAverage(p int, ct, GUID string, format *ConditionalFormatOp Priority: p + 1, StopIfTrue: format.StopIfTrue, Type: validType[format.Type], - AboveAverage: &format.AboveAverage, - DxfID: &format.Format, + AboveAverage: boolPtr(format.AboveAverage), + DxfID: intPtr(format.Format), }, nil } @@ -3634,7 +3634,7 @@ func drawCondFmtDuplicateUniqueValues(p int, ct, GUID string, format *Conditiona Priority: p + 1, StopIfTrue: format.StopIfTrue, Type: validType[format.Type], - DxfID: &format.Format, + DxfID: intPtr(format.Format), }, nil } @@ -3722,7 +3722,7 @@ func drawCondFmtExp(p int, ct, GUID string, format *ConditionalFormatOptions) (* StopIfTrue: format.StopIfTrue, Type: validType[format.Type], Formula: []string{format.Criteria}, - DxfID: &format.Format, + DxfID: intPtr(format.Format), }, nil } @@ -3774,7 +3774,7 @@ func drawCondFmtIconSet(p int, ct, GUID string, format *ConditionalFormatOptions cfRule.IconSet.IconSet = format.IconStyle cfRule.IconSet.Reverse = format.ReverseIcons cfRule.IconSet.ShowValue = boolPtr(!format.IconsOnly) - cfRule.Type = format.Type + cfRule.Type = validType[format.Type] return cfRule, nil } diff --git a/styles_test.go b/styles_test.go index 864f14f297..ae7267a406 100644 --- a/styles_test.go +++ b/styles_test.go @@ -191,13 +191,13 @@ func TestSetConditionalFormat(t *testing.T) { ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: ""} assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", condFmts), "XML syntax error on line 1: element closed by ") // Test creating a conditional format with invalid icon set style - assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "iconSet", IconStyle: "unknown"}}), ErrParameterInvalid.Error()) + assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "unknown"}}), ErrParameterInvalid.Error()) } func TestGetConditionalFormats(t *testing.T) { for _, format := range [][]ConditionalFormatOptions{ {{Type: "cell", Format: 1, Criteria: "greater than", Value: "6"}}, - {{Type: "cell", Format: 1, Criteria: "between", Minimum: "6", Maximum: "8"}}, + {{Type: "cell", Format: 1, Criteria: "between", MinValue: "6", MaxValue: "8"}}, {{Type: "top", Format: 1, Criteria: "=", Value: "6"}}, {{Type: "bottom", Format: 1, Criteria: "=", Value: "6"}}, {{Type: "average", AboveAverage: true, Format: 1, Criteria: "="}}, @@ -208,7 +208,7 @@ func TestGetConditionalFormats(t *testing.T) { {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarOnly: true, BarSolid: true, StopIfTrue: true}}, {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarDirection: "rightToLeft", BarOnly: true, BarSolid: true, StopIfTrue: true}}, {{Type: "formula", Format: 1, Criteria: "="}}, - {{Type: "iconSet", IconStyle: "3Arrows", ReverseIcons: true, IconsOnly: true}}, + {{Type: "icon_set", IconStyle: "3Arrows", ReverseIcons: true, IconsOnly: true}}, } { f := NewFile() err := f.SetConditionalFormat("Sheet1", "A1:A2", format) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 98727de8f7..97bbfdd4a0 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -929,8 +929,6 @@ type ConditionalFormatOptions struct { Format int Criteria string Value string - Minimum string - Maximum string MinType string MidType string MaxType string @@ -940,8 +938,6 @@ type ConditionalFormatOptions struct { MinColor string MidColor string MaxColor string - MinLength string - MaxLength string BarColor string BarBorderColor string BarDirection string From 669c432ca15fb6f9dd33fd3907671141a45c0561 Mon Sep 17 00:00:00 2001 From: Baris Mar Aziz Date: Thu, 23 Feb 2023 23:18:10 +0700 Subject: [PATCH 165/213] This fixes #756, made stream writer skip set cell value when got nil (#1481) --- stream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream.go b/stream.go index bafd7591b1..7cab7305b3 100644 --- a/stream.go +++ b/stream.go @@ -536,7 +536,7 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error { case bool: c.T, c.V = setCellBool(val) case nil: - c.setCellValue("") + return err case []RichTextRun: c.T, c.IS = "inlineStr", &xlsxSI{} c.IS.R, err = setRichText(val) From 65a53b3ec698936ca0049d9533724f15ce119031 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 27 Feb 2023 00:05:36 +0800 Subject: [PATCH 166/213] Breaking changes: replace the type `ShapeParagraph` with `RichTextRun` - This removes the `Color` field from the type `Shape`, and uses the `Fill` instead of it - Remove sharp symbol from hex RGB color - Update unit tests --- calc.go | 2 +- cell.go | 2 +- cell_test.go | 6 +++++- chart_test.go | 2 +- col_test.go | 2 +- comment.go | 10 +++++----- excelize_test.go | 28 ++++++++++++++-------------- pivotTable_test.go | 2 +- rows_test.go | 4 ++-- shape.go | 44 ++++++++++++++++++++++++++------------------ shape_test.go | 35 ++++++++++++++++++----------------- sheetpr_test.go | 4 ++-- sparkline.go | 29 +++++++++++++++-------------- sparkline_test.go | 2 +- stream.go | 2 +- stream_test.go | 10 +++++----- styles.go | 20 ++++++++++---------- styles_test.go | 14 +++++++------- xmlDrawing.go | 14 ++++---------- 19 files changed, 120 insertions(+), 112 deletions(-) diff --git a/calc.go b/calc.go index 70b5409537..4b17d84eae 100644 --- a/calc.go +++ b/calc.go @@ -13489,7 +13489,7 @@ func (fn *formulaFuncs) LENB(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "LENB requires 1 string argument") } bytes := 0 - for _, r := range []rune(argsList.Front().Value.(formulaArg).String) { + for _, r := range argsList.Front().Value.(formulaArg).Value() { b := utf8.RuneLen(r) if b == 1 { bytes++ diff --git a/cell.go b/cell.go index a263b84d8b..a23296b65e 100644 --- a/cell.go +++ b/cell.go @@ -843,7 +843,7 @@ type HyperlinkOpts struct { // } // // Set underline and font color style for the cell. // style, err := f.NewStyle(&excelize.Style{ -// Font: &excelize.Font{Color: "#1265BE", Underline: "single"}, +// Font: &excelize.Font{Color: "1265BE", Underline: "single"}, // }) // if err != nil { // fmt.Println(err) diff --git a/cell_test.go b/cell_test.go index cee218891a..210918cfff 100644 --- a/cell_test.go +++ b/cell_test.go @@ -37,7 +37,7 @@ func TestConcurrency(t *testing.T) { uint64(1<<32 - 1), true, complex64(5 + 10i), })) // Concurrency create style - style, err := f.NewStyle(&Style{Font: &Font{Color: "#1265BE", Underline: "single"}}) + style, err := f.NewStyle(&Style{Font: &Font{Color: "1265BE", Underline: "single"}}) assert.NoError(t, err) // Concurrency set cell style assert.NoError(t, f.SetCellStyle("Sheet1", "A3", "A3", style)) @@ -948,3 +948,7 @@ func TestSharedStringsError(t *testing.T) { return assert.NoError(t, os.Remove(v.(string))) }) } + +func TestSIString(t *testing.T) { + assert.Empty(t, xlsxSI{}.String()) +} diff --git a/chart_test.go b/chart_test.go index 14adb062a6..1f265a3f94 100644 --- a/chart_test.go +++ b/chart_test.go @@ -200,7 +200,7 @@ func TestAddChart(t *testing.T) { sheetName, cell string opts *Chart }{ - {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: "col", Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "#000000"}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "#777777"}}}}, + {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: "col", Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "000000"}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}}}}, {sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: "colStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: "colPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: "col3DClustered", Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Clustered Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, diff --git a/col_test.go b/col_test.go index 4debab0f8e..8e15bebc76 100644 --- a/col_test.go +++ b/col_test.go @@ -320,7 +320,7 @@ func TestSetColStyle(t *testing.T) { f := NewFile() assert.NoError(t, f.SetCellValue("Sheet1", "B2", "Hello")) - styleID, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"#94d3a2"}, Pattern: 1}}) + styleID, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"94D3A2"}, Pattern: 1}}) assert.NoError(t, err) // Test set column style on not exists worksheet assert.EqualError(t, f.SetColStyle("SheetN", "E", styleID), "sheet SheetN does not exist") diff --git a/comment.go b/comment.go index 8206e685e6..40912d07ad 100644 --- a/comment.go +++ b/comment.go @@ -228,8 +228,8 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, ID: "_x0000_s1025", Type: "#_x0000_t202", Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden", - Fillcolor: "#fbf6d6", - Strokecolor: "#edeaa1", + Fillcolor: "#FBF6D6", + Strokecolor: "#EDEAA1", Val: v.Val, } vml.Shape = append(vml.Shape, s) @@ -238,7 +238,7 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, } sp := encodeShape{ Fill: &vFill{ - Color2: "#fbfe82", + Color2: "#FBFE82", Angle: -180, Type: "gradient", Fill: &oFill{ @@ -275,8 +275,8 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, ID: "_x0000_s1025", Type: "#_x0000_t202", Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden", - Fillcolor: "#fbf6d6", - Strokecolor: "#edeaa1", + Fillcolor: "#FBF6D6", + Strokecolor: "#EDEAA1", Val: string(s[13 : len(s)-14]), } vml.Shape = append(vml.Shape, shape) diff --git a/excelize_test.go b/excelize_test.go index ea3a41bfbe..8d9bbc8a5f 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -677,11 +677,11 @@ func TestSetCellStyleBorder(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "J21", "L25", style)) - style, err = f.NewStyle(&Style{Border: []Border{{Type: "left", Color: "0000FF", Style: 2}, {Type: "top", Color: "00FF00", Style: 3}, {Type: "bottom", Color: "FFFF00", Style: 4}, {Type: "right", Color: "FF0000", Style: 5}, {Type: "diagonalDown", Color: "A020F0", Style: 6}, {Type: "diagonalUp", Color: "A020F0", Style: 7}}, Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 1}}) + style, err = f.NewStyle(&Style{Border: []Border{{Type: "left", Color: "0000FF", Style: 2}, {Type: "top", Color: "00FF00", Style: 3}, {Type: "bottom", Color: "FFFF00", Style: 4}, {Type: "right", Color: "FF0000", Style: 5}, {Type: "diagonalDown", Color: "A020F0", Style: 6}, {Type: "diagonalUp", Color: "A020F0", Style: 7}}, Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 1}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "M28", "K24", style)) - style, err = f.NewStyle(&Style{Border: []Border{{Type: "left", Color: "0000FF", Style: 2}, {Type: "top", Color: "00FF00", Style: 3}, {Type: "bottom", Color: "FFFF00", Style: 4}, {Type: "right", Color: "FF0000", Style: 5}, {Type: "diagonalDown", Color: "A020F0", Style: 6}, {Type: "diagonalUp", Color: "A020F0", Style: 7}}, Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 4}}) + style, err = f.NewStyle(&Style{Border: []Border{{Type: "left", Color: "0000FF", Style: 2}, {Type: "top", Color: "00FF00", Style: 3}, {Type: "bottom", Color: "FFFF00", Style: 4}, {Type: "right", Color: "FF0000", Style: 5}, {Type: "diagonalDown", Color: "A020F0", Style: 6}, {Type: "diagonalUp", Color: "A020F0", Style: 7}}, Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 4}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "M28", "K24", style)) @@ -721,7 +721,7 @@ func TestSetCellStyleBorder(t *testing.T) { }, Fill: Fill{ Type: "pattern", - Color: []string{"#E0EBF5"}, + Color: []string{"E0EBF5"}, Pattern: 1, }, }) @@ -767,7 +767,7 @@ func TestSetCellStyleNumberFormat(t *testing.T) { } else { assert.NoError(t, f.SetCellValue("Sheet2", c, val)) } - style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 5}, NumFmt: d}) + style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 5}, NumFmt: d}) if !assert.NoError(t, err) { t.FailNow() } @@ -839,7 +839,7 @@ func TestSetCellStyleCustomNumberFormat(t *testing.T) { style, err := f.NewStyle(&Style{CustomNumFmt: &customNumFmt}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) - style, err = f.NewStyle(&Style{CustomNumFmt: &customNumFmt, Font: &Font{Color: "#9A0511"}}) + style, err = f.NewStyle(&Style{CustomNumFmt: &customNumFmt, Font: &Font{Color: "9A0511"}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) @@ -855,11 +855,11 @@ func TestSetCellStyleFill(t *testing.T) { var style int // Test set fill for cell with invalid parameter - style, err = f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 6}}) + style, err = f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 6}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) - style, err = f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF"}, Shading: 1}}) + style, err = f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF"}, Shading: 1}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) @@ -879,7 +879,7 @@ func TestSetCellStyleFont(t *testing.T) { assert.NoError(t, err) var style int - style, err = f.NewStyle(&Style{Font: &Font{Bold: true, Italic: true, Family: "Times New Roman", Size: 36, Color: "#777777", Underline: "single"}}) + style, err = f.NewStyle(&Style{Font: &Font{Bold: true, Italic: true, Family: "Times New Roman", Size: 36, Color: "777777", Underline: "single"}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet2", "A1", "A1", style)) @@ -899,7 +899,7 @@ func TestSetCellStyleFont(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet2", "A4", "A4", style)) - style, err = f.NewStyle(&Style{Font: &Font{Color: "#777777", Strike: true}}) + style, err = f.NewStyle(&Style{Font: &Font{Color: "777777", Strike: true}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet2", "A5", "A5", style)) @@ -1002,19 +1002,19 @@ func TestConditionalFormat(t *testing.T) { var format1, format2, format3, format4 int var err error // Rose format for bad conditional - format1, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "#9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"#FEC7CE"}, Pattern: 1}}) + format1, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}}) assert.NoError(t, err) // Light yellow format for neutral conditional - format2, err = f.NewConditionalStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"#FEEAA0"}, Pattern: 1}}) + format2, err = f.NewConditionalStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"FEEAA0"}, Pattern: 1}}) assert.NoError(t, err) // Light green format for good conditional - format3, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "#09600B"}, Fill: Fill{Type: "pattern", Color: []string{"#C7EECF"}, Pattern: 1}}) + format3, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "09600B"}, Fill: Fill{Type: "pattern", Color: []string{"C7EECF"}, Pattern: 1}}) assert.NoError(t, err) // conditional style with align and left border - format4, err = f.NewConditionalStyle(&Style{Alignment: &Alignment{WrapText: true}, Border: []Border{{Type: "left", Color: "#000000", Style: 1}}}) + format4, err = f.NewConditionalStyle(&Style{Alignment: &Alignment{WrapText: true}, Border: []Border{{Type: "left", Color: "000000", Style: 1}}}) assert.NoError(t, err) // Color scales: 2 color @@ -1206,7 +1206,7 @@ func TestConditionalFormat(t *testing.T) { f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) - _, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "#9A0511"}, Fill: Fill{Type: "", Color: []string{"#FEC7CE"}, Pattern: 1}}) + _, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "", Color: []string{"FEC7CE"}, Pattern: 1}}) assert.NoError(t, err) assert.NoError(t, f.Close()) } diff --git a/pivotTable_test.go b/pivotTable_test.go index 520d56dee1..24ccb4a0bc 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -70,7 +70,7 @@ func TestAddPivotTable(t *testing.T) { })) assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet1!$G$39:$W$52", + PivotTableRange: "Sheet1!$G$42:$W$55", Rows: []PivotTableField{{Data: "Month"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}}, diff --git a/rows_test.go b/rows_test.go index 95e59d9932..8b5008f906 100644 --- a/rows_test.go +++ b/rows_test.go @@ -990,9 +990,9 @@ func TestCheckRow(t *testing.T) { func TestSetRowStyle(t *testing.T) { f := NewFile() - style1, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"#63BE7B"}, Pattern: 1}}) + style1, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"63BE7B"}, Pattern: 1}}) assert.NoError(t, err) - style2, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"#E0EBF5"}, Pattern: 1}}) + style2, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"E0EBF5"}, Pattern: 1}}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "B2", "B2", style1)) assert.EqualError(t, f.SetRowStyle("Sheet1", 5, -1, style2), newInvalidRowNumberError(-1).Error()) diff --git a/shape.go b/shape.go index d194e55751..b0d449b281 100644 --- a/shape.go +++ b/shape.go @@ -54,24 +54,24 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { // lineWidth := 1.2 // err := f.AddShape("Sheet1", "G6", // &excelize.Shape{ -// Type: "rect", -// Color: excelize.ShapeColor{Line: "#4286f4", Fill: "#8eb9ff"}, -// Paragraph: []excelize.ShapeParagraph{ +// Type: "rect", +// Line: excelize.ShapeLine{Color: "4286F4", Width: &lineWidth}, +// Fill: excelize.Fill{Color: []string{"8EB9FF"}}, +// Paragraph: []excelize.RichTextRun{ // { // Text: "Rectangle Shape", -// Font: excelize.Font{ +// Font: &excelize.Font{ // Bold: true, // Italic: true, // Family: "Times New Roman", // Size: 18, -// Color: "#777777", +// Color: "777777", // Underline: "sng", // }, // }, // }, // Width: 180, // Height: 40, -// Line: excelize.ShapeLine{Width: &lineWidth}, // }, // ) // @@ -352,6 +352,10 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro to.RowOff = y2 * EMU twoCellAnchor.From = &from twoCellAnchor.To = &to + var solidColor string + if len(opts.Fill.Color) == 1 { + solidColor = opts.Fill.Color[0] + } shape := xdrSp{ Macro: opts.Macro, NvSpPr: &xdrNvSpPr{ @@ -369,9 +373,9 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro }, }, Style: &xdrStyle{ - LnRef: setShapeRef(opts.Color.Line, 2), - FillRef: setShapeRef(opts.Color.Fill, 1), - EffectRef: setShapeRef(opts.Color.Effect, 0), + LnRef: setShapeRef(opts.Line.Color, 2), + FillRef: setShapeRef(solidColor, 1), + EffectRef: setShapeRef("", 0), FontRef: &aFontRef{ Idx: "minor", SchemeClr: &attrValString{ @@ -399,15 +403,15 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro return err } if len(opts.Paragraph) < 1 { - opts.Paragraph = []ShapeParagraph{ + opts.Paragraph = []RichTextRun{ { - Font: Font{ + Font: &Font{ Bold: false, Italic: false, Underline: "none", Family: defaultFont, Size: 11, - Color: "#000000", + Color: "000000", }, Text: " ", }, @@ -415,7 +419,11 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro } for _, p := range opts.Paragraph { u := "none" - if idx := inStrSlice(supportedDrawingUnderlineTypes, p.Font.Underline, true); idx != -1 { + font := &Font{} + if p.Font != nil { + font = p.Font + } + if idx := inStrSlice(supportedDrawingUnderlineTypes, font.Underline, true); idx != -1 { u = supportedDrawingUnderlineTypes[idx] } text := p.Text @@ -425,13 +433,13 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro paragraph := &aP{ R: &aR{ RPr: aRPr{ - I: p.Font.Italic, - B: p.Font.Bold, + I: font.Italic, + B: font.Bold, Lang: "en-US", AltLang: "en-US", U: u, - Sz: p.Font.Size * 100, - Latin: &xlsxCTTextFont{Typeface: p.Font.Family}, + Sz: font.Size * 100, + Latin: &xlsxCTTextFont{Typeface: font.Family}, }, T: text, }, @@ -439,7 +447,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro Lang: "en-US", }, } - srgbClr := strings.ReplaceAll(strings.ToUpper(p.Font.Color), "#", "") + srgbClr := strings.ReplaceAll(strings.ToUpper(font.Color), "#", "") if len(srgbClr) == 6 { paragraph.R.RPr.SolidFill = &aSolidFill{ SrgbClr: &attrValString{ diff --git a/shape_test.go b/shape_test.go index 436140865e..c9ba9d90a2 100644 --- a/shape_test.go +++ b/shape_test.go @@ -14,26 +14,27 @@ func TestAddShape(t *testing.T) { } shape := &Shape{ Type: "rect", - Paragraph: []ShapeParagraph{ - {Text: "Rectangle", Font: Font{Color: "CD5C5C"}}, - {Text: "Shape", Font: Font{Bold: true, Color: "2980B9"}}, + Paragraph: []RichTextRun{ + {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}}, + {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}}, }, } assert.NoError(t, f.AddShape("Sheet1", "A30", shape)) - assert.NoError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []ShapeParagraph{{Text: "Rectangle"}, {}}})) + assert.NoError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}})) assert.NoError(t, f.AddShape("Sheet1", "C30", &Shape{Type: "rect"})) assert.EqualError(t, f.AddShape("Sheet3", "H1", &Shape{ - Type: "ellipseRibbon", - Color: ShapeColor{Line: "#4286f4", Fill: "#8eb9ff"}, - Paragraph: []ShapeParagraph{ + Type: "ellipseRibbon", + Line: ShapeLine{Color: "4286F4"}, + Fill: Fill{Color: []string{"8EB9FF"}}, + Paragraph: []RichTextRun{ { - Font: Font{ + Font: &Font{ Bold: true, Italic: true, Family: "Times New Roman", Size: 36, - Color: "#777777", + Color: "777777", Underline: "single", }, }, @@ -49,22 +50,22 @@ func TestAddShape(t *testing.T) { lineWidth := 1.2 assert.NoError(t, f.AddShape("Sheet1", "A1", &Shape{ - Type: "ellipseRibbon", - Color: ShapeColor{Line: "#4286f4", Fill: "#8eb9ff"}, - Paragraph: []ShapeParagraph{ + Type: "ellipseRibbon", + Line: ShapeLine{Color: "4286F4", Width: &lineWidth}, + Fill: Fill{Color: []string{"8EB9FF"}}, + Paragraph: []RichTextRun{ { - Font: Font{ + Font: &Font{ Bold: true, Italic: true, Family: "Times New Roman", Size: 36, - Color: "#777777", + Color: "777777", Underline: "single", }, }, }, Height: 90, - Line: ShapeLine{Width: &lineWidth}, })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) // Test add shape with invalid sheet name @@ -72,12 +73,12 @@ func TestAddShape(t *testing.T) { // Test add shape with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []ShapeParagraph{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") // Test add shape with unsupported charset content types f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []ShapeParagraph{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") } func TestAddDrawingShape(t *testing.T) { diff --git a/sheetpr_test.go b/sheetpr_test.go index daf6c191f6..5491e78694 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -58,7 +58,7 @@ func TestSetSheetProps(t *testing.T) { AutoPageBreaks: enable, FitToPage: enable, TabColorIndexed: intPtr(1), - TabColorRGB: stringPtr("#FFFF00"), + TabColorRGB: stringPtr("FFFF00"), TabColorTheme: intPtr(1), TabColorTint: float64Ptr(1), OutlineSummaryBelow: enable, @@ -79,7 +79,7 @@ func TestSetSheetProps(t *testing.T) { ws.(*xlsxWorksheet).SheetPr = nil assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{FitToPage: enable})) ws.(*xlsxWorksheet).SheetPr = nil - assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{TabColorRGB: stringPtr("#FFFF00")})) + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{TabColorRGB: stringPtr("FFFF00")})) ws.(*xlsxWorksheet).SheetPr = nil assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{TabColorTheme: intPtr(1)})) ws.(*xlsxWorksheet).SheetPr = nil diff --git a/sparkline.go b/sparkline.go index b9879ac81d..51bd106274 100644 --- a/sparkline.go +++ b/sparkline.go @@ -374,20 +374,21 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // // The following shows the formatting options of sparkline supported by excelize: // -// Parameter | Description -// -----------+-------------------------------------------- -// Location | Required, must have the same number with 'Range' parameter -// Range | Required, must have the same number with 'Location' parameter -// Type | Enumeration value: line, column, win_loss -// Style | Value range: 0 - 35 -// Hight | Toggle sparkline high points -// Low | Toggle sparkline low points -// First | Toggle sparkline first points -// Last | Toggle sparkline last points -// Negative | Toggle sparkline negative points -// Markers | Toggle sparkline markers -// ColorAxis | An RGB Color is specified as RRGGBB -// Axis | Show sparkline axis +// Parameter | Description +// -------------+-------------------------------------------- +// Location | Required, must have the same number with 'Range' parameter +// Range | Required, must have the same number with 'Location' parameter +// Type | Enumeration value: line, column, win_loss +// Style | Value range: 0 - 35 +// Hight | Toggle sparkline high points +// Low | Toggle sparkline low points +// First | Toggle sparkline first points +// Last | Toggle sparkline last points +// Negative | Toggle sparkline negative points +// Markers | Toggle sparkline markers +// Axis | Used to specify if show horizontal axis +// Reverse | Used to specify if enable plot data right-to-left +// SeriesColor | An RGB Color is specified as RRGGBB func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error { var ( err error diff --git a/sparkline_test.go b/sparkline_test.go index b1d3d18672..0d1511d040 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -136,7 +136,7 @@ func TestAddSparkline(t *testing.T) { Location: []string{"A18"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", - SeriesColor: "#E965E0", + SeriesColor: "E965E0", })) assert.NoError(t, f.SetCellValue("Sheet1", "B20", "A win/loss sparkline.")) diff --git a/stream.go b/stream.go index 7cab7305b3..0577a7c918 100644 --- a/stream.go +++ b/stream.go @@ -58,7 +58,7 @@ type StreamWriter struct { // fmt.Println(err) // return // } -// styleID, err := file.NewStyle(&excelize.Style{Font: &excelize.Font{Color: "#777777"}}) +// styleID, err := file.NewStyle(&excelize.Style{Font: &excelize.Font{Color: "777777"}}) // if err != nil { // fmt.Println(err) // return diff --git a/stream_test.go b/stream_test.go index 0e69055275..da25bb9db7 100644 --- a/stream_test.go +++ b/stream_test.go @@ -56,7 +56,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, streamWriter.SetRow("A3", row)) // Test set cell with style and rich text - styleID, err := file.NewStyle(&Style{Font: &Font{Color: "#777777"}}) + styleID, err := file.NewStyle(&Style{Font: &Font{Color: "777777"}}) assert.NoError(t, err) assert.NoError(t, streamWriter.SetRow("A4", []interface{}{ Cell{StyleID: styleID}, @@ -67,8 +67,8 @@ func TestStreamWriter(t *testing.T) { &Cell{StyleID: styleID, Value: "cell <>&'\""}, &Cell{Formula: "SUM(A10,B10)"}, []RichTextRun{ - {Text: "Rich ", Font: &Font{Color: "2354e8"}}, - {Text: "Text", Font: &Font{Color: "e83723"}}, + {Text: "Rich ", Font: &Font{Color: "2354E8"}}, + {Text: "Text", Font: &Font{Color: "E83723"}}, }, })) assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()})) @@ -318,9 +318,9 @@ func TestStreamSetRowWithStyle(t *testing.T) { assert.NoError(t, file.Close()) }() zeroStyleID := 0 - grayStyleID, err := file.NewStyle(&Style{Font: &Font{Color: "#777777"}}) + grayStyleID, err := file.NewStyle(&Style{Font: &Font{Color: "777777"}}) assert.NoError(t, err) - blueStyleID, err := file.NewStyle(&Style{Font: &Font{Color: "#0000FF"}}) + blueStyleID, err := file.NewStyle(&Style{Font: &Font{Color: "0000FF"}}) assert.NoError(t, err) streamWriter, err := file.NewStreamWriter("Sheet1") diff --git a/styles.go b/styles.go index 7483d30fe4..3c02e2d38c 100644 --- a/styles.go +++ b/styles.go @@ -2702,7 +2702,7 @@ func (f *File) GetCellStyle(sheet, cell string) (int, error) { // Sheet1: // // style, err := f.NewStyle(&excelize.Style{ -// Fill: excelize.Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 1}, +// Fill: excelize.Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 1}, // }) // if err != nil { // fmt.Println(err) @@ -2712,7 +2712,7 @@ func (f *File) GetCellStyle(sheet, cell string) (int, error) { // Set solid style pattern fill for cell H9 on Sheet1: // // style, err := f.NewStyle(&excelize.Style{ -// Fill: excelize.Fill{Type: "pattern", Color: []string{"#E0EBF5"}, Pattern: 1}, +// Fill: excelize.Fill{Type: "pattern", Color: []string{"E0EBF5"}, Pattern: 1}, // }) // if err != nil { // fmt.Println(err) @@ -2758,7 +2758,7 @@ func (f *File) GetCellStyle(sheet, cell string) (int, error) { // Italic: true, // Family: "Times New Roman", // Size: 36, -// Color: "#777777", +// Color: "777777", // }, // }) // if err != nil { @@ -2945,9 +2945,9 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // // format, err := f.NewConditionalStyle( // &excelize.Style{ -// Font: &excelize.Font{Color: "#9A0511"}, +// Font: &excelize.Font{Color: "9A0511"}, // Fill: excelize.Fill{ -// Type: "pattern", Color: []string{"#FEC7CE"}, Pattern: 1, +// Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1, // }, // }, // ) @@ -2972,7 +2972,7 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // // Rose format for bad conditional. // format1, err := f.NewConditionalStyle( // &excelize.Style{ -// Font: &excelize.Font{Color: "#9A0511"}, +// Font: &excelize.Font{Color: "9A0511"}, // Fill: excelize.Fill{ // Type: "pattern", Color: []string{"#FEC7CE"}, Pattern: 1, // }, @@ -2982,9 +2982,9 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // // Light yellow format for neutral conditional. // format2, err := f.NewConditionalStyle( // &excelize.Style{ -// Font: &excelize.Font{Color: "#9B5713"}, +// Font: &excelize.Font{Color: "9B5713"}, // Fill: excelize.Fill{ -// Type: "pattern", Color: []string{"#FEEAA0"}, Pattern: 1, +// Type: "pattern", Color: []string{"FEEAA0"}, Pattern: 1, // }, // }, // ) @@ -2992,9 +2992,9 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // // Light green format for good conditional. // format3, err := f.NewConditionalStyle( // &excelize.Style{ -// Font: &excelize.Font{Color: "#09600B"}, +// Font: &excelize.Font{Color: "09600B"}, // Fill: excelize.Fill{ -// Type: "pattern", Color: []string{"#C7EECF"}, Pattern: 1, +// Type: "pattern", Color: []string{"C7EECF"}, Pattern: 1, // }, // }, // ) diff --git a/styles_test.go b/styles_test.go index ae7267a406..257547a64c 100644 --- a/styles_test.go +++ b/styles_test.go @@ -20,7 +20,7 @@ func TestStyleFill(t *testing.T) { expectFill: false, }, { label: "fill", - format: &Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"#000000"}}}, + format: &Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"000000"}}}, expectFill: true, }} @@ -39,9 +39,9 @@ func TestStyleFill(t *testing.T) { } } f := NewFile() - styleID1, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"#000000"}}}) + styleID1, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"000000"}}}) assert.NoError(t, err) - styleID2, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"#000000"}}}) + styleID2, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"000000"}}}) assert.NoError(t, err) assert.Equal(t, styleID1, styleID2) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleFill.xlsx"))) @@ -230,7 +230,7 @@ func TestUnsetConditionalFormat(t *testing.T) { f := NewFile() assert.NoError(t, f.SetCellValue("Sheet1", "A1", 7)) assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10")) - format, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "#9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"#FEC7CE"}, Pattern: 1}}) + format, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}}) assert.NoError(t, err) assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", []ConditionalFormatOptions{{Type: "cell", Criteria: ">", Format: format, Value: "6"}})) assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10")) @@ -246,12 +246,12 @@ func TestNewStyle(t *testing.T) { f := NewFile() for i := 0; i < 18; i++ { _, err := f.NewStyle(&Style{ - Fill: Fill{Type: "gradient", Color: []string{"#FFFFFF", "#4E71BE"}, Shading: i}, + Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "4E71BE"}, Shading: i}, }) assert.NoError(t, err) } f = NewFile() - styleID, err := f.NewStyle(&Style{Font: &Font{Bold: true, Italic: true, Family: "Times New Roman", Size: 36, Color: "#777777"}}) + styleID, err := f.NewStyle(&Style{Font: &Font{Bold: true, Italic: true, Family: "Times New Roman", Size: 36, Color: "777777"}}) assert.NoError(t, err) styles, err := f.stylesReader() assert.NoError(t, err) @@ -360,7 +360,7 @@ func TestNewConditionalStyle(t *testing.T) { // Test create conditional style with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) - _, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "#9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"#FEC7CE"}, Pattern: 1}}) + _, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}}) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } diff --git a/xmlDrawing.go b/xmlDrawing.go index cc9585a629..3130833f10 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -598,21 +598,14 @@ type GraphicOptions struct { // Shape directly maps the format settings of the shape. type Shape struct { - Macro string Type string + Macro string Width uint Height uint Format GraphicOptions - Color ShapeColor + Fill Fill Line ShapeLine - Paragraph []ShapeParagraph -} - -// ShapeParagraph directly maps the format settings of the paragraph in -// the shape. -type ShapeParagraph struct { - Font Font - Text string + Paragraph []RichTextRun } // ShapeColor directly maps the color settings of the shape. @@ -624,5 +617,6 @@ type ShapeColor struct { // ShapeLine directly maps the line settings of the shape. type ShapeLine struct { + Color string Width *float64 } From f707b2d2da77ff3715ab729434b59f374b6e8c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B6=9B?= <44195851+doingNobb@users.noreply.github.com> Date: Wed, 1 Mar 2023 13:25:17 +0800 Subject: [PATCH 167/213] This closes #1484, fix the formula calc result issue (#1485) - Optimize variable name for data validation --- calc.go | 31 +++++------ datavalidation.go | 82 ++++++++++++++-------------- datavalidation_test.go | 120 ++++++++++++++++++++--------------------- 3 files changed, 117 insertions(+), 116 deletions(-) diff --git a/calc.go b/calc.go index 4b17d84eae..7b8dcf525d 100644 --- a/calc.go +++ b/calc.go @@ -1532,6 +1532,22 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e value string err error ) + ref := fmt.Sprintf("%s!%s", sheet, cell) + if formula, _ := f.GetCellFormula(sheet, cell); len(formula) != 0 { + ctx.Lock() + if ctx.entry != ref { + if ctx.iterations[ref] <= f.options.MaxCalcIterations { + ctx.iterations[ref]++ + ctx.Unlock() + arg, _ = f.calcCellValue(ctx, sheet, cell) + ctx.iterationsCache[ref] = arg + return arg, nil + } + ctx.Unlock() + return ctx.iterationsCache[ref], nil + } + ctx.Unlock() + } if value, err = f.GetCellValue(sheet, cell, Options{RawCellValue: true}); err != nil { return arg, err } @@ -1547,21 +1563,6 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e return arg.ToNumber(), err case CellTypeInlineString, CellTypeSharedString: return arg, err - case CellTypeFormula: - ref := fmt.Sprintf("%s!%s", sheet, cell) - if ctx.entry != ref { - ctx.Lock() - if ctx.iterations[ref] <= ctx.maxCalcIterations { - ctx.iterations[ref]++ - ctx.Unlock() - arg, _ = f.calcCellValue(ctx, sheet, cell) - ctx.iterationsCache[ref] = arg - return arg, nil - } - ctx.Unlock() - return ctx.iterationsCache[ref], nil - } - fallthrough default: return newEmptyFormulaArg(), err } diff --git a/datavalidation.go b/datavalidation.go index 1201b4fa8b..ac4aaec57c 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -88,9 +88,9 @@ func NewDataValidation(allowBlank bool) *DataValidation { } // SetError set error notice. -func (dd *DataValidation) SetError(style DataValidationErrorStyle, title, msg string) { - dd.Error = &msg - dd.ErrorTitle = &title +func (dv *DataValidation) SetError(style DataValidationErrorStyle, title, msg string) { + dv.Error = &msg + dv.ErrorTitle = &title strStyle := styleStop switch style { case DataValidationErrorStyleStop: @@ -101,31 +101,31 @@ func (dd *DataValidation) SetError(style DataValidationErrorStyle, title, msg st strStyle = styleInformation } - dd.ShowErrorMessage = true - dd.ErrorStyle = &strStyle + dv.ShowErrorMessage = true + dv.ErrorStyle = &strStyle } // SetInput set prompt notice. -func (dd *DataValidation) SetInput(title, msg string) { - dd.ShowInputMessage = true - dd.PromptTitle = &title - dd.Prompt = &msg +func (dv *DataValidation) SetInput(title, msg string) { + dv.ShowInputMessage = true + dv.PromptTitle = &title + dv.Prompt = &msg } // SetDropList data validation list. -func (dd *DataValidation) SetDropList(keys []string) error { +func (dv *DataValidation) SetDropList(keys []string) error { formula := strings.Join(keys, ",") if MaxFieldLength < len(utf16.Encode([]rune(formula))) { return ErrDataValidationFormulaLength } - dd.Formula1 = fmt.Sprintf(`"%s"`, formulaEscaper.Replace(formula)) - dd.Type = convDataValidationType(typeList) + dv.Formula1 = fmt.Sprintf(`"%s"`, formulaEscaper.Replace(formula)) + dv.Type = convDataValidationType(typeList) return nil } // SetRange provides function to set data validation range in drop list, only // accepts int, float64, or string data type formula argument. -func (dd *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o DataValidationOperator) error { +func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o DataValidationOperator) error { var formula1, formula2 string switch v := f1.(type) { case int: @@ -153,9 +153,9 @@ func (dd *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o D default: return ErrParameterInvalid } - dd.Formula1, dd.Formula2 = formula1, formula2 - dd.Type = convDataValidationType(t) - dd.Operator = convDataValidationOperator(o) + dv.Formula1, dv.Formula2 = formula1, formula2 + dv.Type = convDataValidationType(t) + dv.Operator = convDataValidationOperator(o) return nil } @@ -166,21 +166,21 @@ func (dd *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o D // Sheet1!A7:B8 with validation criteria source Sheet1!E1:E3 settings, create // in-cell dropdown by allowing list source: // -// dvRange := excelize.NewDataValidation(true) -// dvRange.Sqref = "A7:B8" -// dvRange.SetSqrefDropList("$E$1:$E$3") -// f.AddDataValidation("Sheet1", dvRange) -func (dd *DataValidation) SetSqrefDropList(sqref string) { - dd.Formula1 = fmt.Sprintf("%s", sqref) - dd.Type = convDataValidationType(typeList) +// dv := excelize.NewDataValidation(true) +// dv.Sqref = "A7:B8" +// dv.SetSqrefDropList("$E$1:$E$3") +// err := f.AddDataValidation("Sheet1", dv) +func (dv *DataValidation) SetSqrefDropList(sqref string) { + dv.Formula1 = fmt.Sprintf("%s", sqref) + dv.Type = convDataValidationType(typeList) } // SetSqref provides function to set data validation range in drop list. -func (dd *DataValidation) SetSqref(sqref string) { - if dd.Sqref == "" { - dd.Sqref = sqref +func (dv *DataValidation) SetSqref(sqref string) { + if dv.Sqref == "" { + dv.Sqref = sqref } else { - dd.Sqref = fmt.Sprintf("%s %s", dd.Sqref, sqref) + dv.Sqref = fmt.Sprintf("%s %s", dv.Sqref, sqref) } } @@ -224,28 +224,28 @@ func convDataValidationOperator(o DataValidationOperator) string { // settings, show error alert after invalid data is entered with "Stop" style // and custom title "error body": // -// dvRange := excelize.NewDataValidation(true) -// dvRange.Sqref = "A1:B2" -// dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorBetween) -// dvRange.SetError(excelize.DataValidationErrorStyleStop, "error title", "error body") -// err := f.AddDataValidation("Sheet1", dvRange) +// dv := excelize.NewDataValidation(true) +// dv.Sqref = "A1:B2" +// dv.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorBetween) +// dv.SetError(excelize.DataValidationErrorStyleStop, "error title", "error body") +// err := f.AddDataValidation("Sheet1", dv) // // Example 2, set data validation on Sheet1!A3:B4 with validation criteria // settings, and show input message when cell is selected: // -// dvRange = excelize.NewDataValidation(true) -// dvRange.Sqref = "A3:B4" -// dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorGreaterThan) -// dvRange.SetInput("input title", "input body") -// err = f.AddDataValidation("Sheet1", dvRange) +// dv = excelize.NewDataValidation(true) +// dv.Sqref = "A3:B4" +// dv.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorGreaterThan) +// dv.SetInput("input title", "input body") +// err = f.AddDataValidation("Sheet1", dv) // // Example 3, set data validation on Sheet1!A5:B6 with validation criteria // settings, create in-cell dropdown by allowing list source: // -// dvRange = excelize.NewDataValidation(true) -// dvRange.Sqref = "A5:B6" -// dvRange.SetDropList([]string{"1", "2", "3"}) -// err = f.AddDataValidation("Sheet1", dvRange) +// dv = excelize.NewDataValidation(true) +// dv.Sqref = "A5:B6" +// dv.SetDropList([]string{"1", "2", "3"}) +// err = f.AddDataValidation("Sheet1", dv) func (f *File) AddDataValidation(sheet string, dv *DataValidation) error { ws, err := f.workSheetReader(sheet) if err != nil { diff --git a/datavalidation_test.go b/datavalidation_test.go index 66855f74b2..4987f81864 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -25,13 +25,13 @@ func TestDataValidation(t *testing.T) { f := NewFile() - dvRange := NewDataValidation(true) - dvRange.Sqref = "A1:B2" - assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween)) - dvRange.SetError(DataValidationErrorStyleStop, "error title", "error body") - dvRange.SetError(DataValidationErrorStyleWarning, "error title", "error body") - dvRange.SetError(DataValidationErrorStyleInformation, "error title", "error body") - assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + dv := NewDataValidation(true) + dv.Sqref = "A1:B2" + assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween)) + dv.SetError(DataValidationErrorStyleStop, "error title", "error body") + dv.SetError(DataValidationErrorStyleWarning, "error title", "error body") + dv.SetError(DataValidationErrorStyleInformation, "error title", "error body") + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) dataValidations, err := f.GetDataValidations("Sheet1") assert.NoError(t, err) @@ -39,11 +39,11 @@ func TestDataValidation(t *testing.T) { assert.NoError(t, f.SaveAs(resultFile)) - dvRange = NewDataValidation(true) - dvRange.Sqref = "A3:B4" - assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) - dvRange.SetInput("input title", "input body") - assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + dv = NewDataValidation(true) + dv.Sqref = "A3:B4" + assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) + dv.SetInput("input title", "input body") + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) dataValidations, err = f.GetDataValidations("Sheet1") assert.NoError(t, err) @@ -55,11 +55,11 @@ func TestDataValidation(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.SetSheetRow("Sheet2", "A2", &[]interface{}{"B2", 1})) assert.NoError(t, f.SetSheetRow("Sheet2", "A3", &[]interface{}{"B3", 3})) - dvRange = NewDataValidation(true) - dvRange.Sqref = "A1:B1" - assert.NoError(t, dvRange.SetRange("INDIRECT($A$2)", "INDIRECT($A$3)", DataValidationTypeWhole, DataValidationOperatorBetween)) - dvRange.SetError(DataValidationErrorStyleStop, "error title", "error body") - assert.NoError(t, f.AddDataValidation("Sheet2", dvRange)) + dv = NewDataValidation(true) + dv.Sqref = "A1:B1" + assert.NoError(t, dv.SetRange("INDIRECT($A$2)", "INDIRECT($A$3)", DataValidationTypeWhole, DataValidationOperatorBetween)) + dv.SetError(DataValidationErrorStyleStop, "error title", "error body") + assert.NoError(t, f.AddDataValidation("Sheet2", dv)) dataValidations, err = f.GetDataValidations("Sheet1") assert.NoError(t, err) assert.Equal(t, len(dataValidations), 2) @@ -67,8 +67,8 @@ func TestDataValidation(t *testing.T) { assert.NoError(t, err) assert.Equal(t, len(dataValidations), 1) - dvRange = NewDataValidation(true) - dvRange.Sqref = "A5:B6" + dv = NewDataValidation(true) + dv.Sqref = "A5:B6" for _, listValid := range [][]string{ {"1", "2", "3"}, {strings.Repeat("&", MaxFieldLength)}, @@ -76,14 +76,14 @@ func TestDataValidation(t *testing.T) { {strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"}, {`A<`, `B>`, `C"`, "D\t", `E'`, `F`}, } { - dvRange.Formula1 = "" - assert.NoError(t, dvRange.SetDropList(listValid), + dv.Formula1 = "" + assert.NoError(t, dv.SetDropList(listValid), "SetDropList failed for valid input %v", listValid) - assert.NotEqual(t, "", dvRange.Formula1, + assert.NotEqual(t, "", dv.Formula1, "Formula1 should not be empty for valid input %v", listValid) } - assert.Equal(t, `"A<,B>,C"",D ,E',F"`, dvRange.Formula1) - assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.Equal(t, `"A<,B>,C"",D ,E',F"`, dv.Formula1) + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) dataValidations, err = f.GetDataValidations("Sheet1") assert.NoError(t, err) @@ -113,29 +113,29 @@ func TestDataValidationError(t *testing.T) { assert.NoError(t, f.SetCellStr("Sheet1", "E2", "E2")) assert.NoError(t, f.SetCellStr("Sheet1", "E3", "E3")) - dvRange := NewDataValidation(true) - dvRange.SetSqref("A7:B8") - dvRange.SetSqref("A7:B8") - dvRange.SetSqrefDropList("$E$1:$E$3") + dv := NewDataValidation(true) + dv.SetSqref("A7:B8") + dv.SetSqref("A7:B8") + dv.SetSqrefDropList("$E$1:$E$3") - assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) - dvRange = NewDataValidation(true) - err := dvRange.SetDropList(make([]string, 258)) - if dvRange.Formula1 != "" { + dv = NewDataValidation(true) + err := dv.SetDropList(make([]string, 258)) + if dv.Formula1 != "" { t.Errorf("data validation error. Formula1 must be empty!") return } assert.EqualError(t, err, ErrDataValidationFormulaLength.Error()) - assert.EqualError(t, dvRange.SetRange(nil, 20, DataValidationTypeWhole, DataValidationOperatorBetween), ErrParameterInvalid.Error()) - assert.EqualError(t, dvRange.SetRange(10, nil, DataValidationTypeWhole, DataValidationOperatorBetween), ErrParameterInvalid.Error()) - assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) - dvRange.SetSqref("A9:B10") + assert.EqualError(t, dv.SetRange(nil, 20, DataValidationTypeWhole, DataValidationOperatorBetween), ErrParameterInvalid.Error()) + assert.EqualError(t, dv.SetRange(10, nil, DataValidationTypeWhole, DataValidationOperatorBetween), ErrParameterInvalid.Error()) + assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) + dv.SetSqref("A9:B10") - assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) // Test width invalid data validation formula - prevFormula1 := dvRange.Formula1 + prevFormula1 := dv.Formula1 for _, keys := range [][]string{ make([]string, 257), {strings.Repeat("s", 256)}, @@ -143,19 +143,19 @@ func TestDataValidationError(t *testing.T) { {strings.Repeat("\U0001F600", 128)}, {strings.Repeat("\U0001F600", 127), "s"}, } { - err = dvRange.SetDropList(keys) - assert.Equal(t, prevFormula1, dvRange.Formula1, + err = dv.SetDropList(keys) + assert.Equal(t, prevFormula1, dv.Formula1, "Formula1 should be unchanged for invalid input %v", keys) assert.EqualError(t, err, ErrDataValidationFormulaLength.Error()) } - assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) - assert.NoError(t, dvRange.SetRange( + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + assert.NoError(t, dv.SetRange( -math.MaxFloat32, math.MaxFloat32, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) - assert.EqualError(t, dvRange.SetRange( + assert.EqualError(t, dv.SetRange( -math.MaxFloat64, math.MaxFloat32, DataValidationTypeWhole, DataValidationOperatorGreaterThan), ErrDataValidationRange.Error()) - assert.EqualError(t, dvRange.SetRange( + assert.EqualError(t, dv.SetRange( math.SmallestNonzeroFloat64, math.MaxFloat64, DataValidationTypeWhole, DataValidationOperatorGreaterThan), ErrDataValidationRange.Error()) assert.NoError(t, f.SaveAs(resultFile)) @@ -173,33 +173,33 @@ func TestDeleteDataValidation(t *testing.T) { f := NewFile() assert.NoError(t, f.DeleteDataValidation("Sheet1", "A1:B2")) - dvRange := NewDataValidation(true) - dvRange.Sqref = "A1:B2" - assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween)) - dvRange.SetInput("input title", "input body") - assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + dv := NewDataValidation(true) + dv.Sqref = "A1:B2" + assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween)) + dv.SetInput("input title", "input body") + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) assert.NoError(t, f.DeleteDataValidation("Sheet1", "A1:B2")) - dvRange.Sqref = "A1" - assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + dv.Sqref = "A1" + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) assert.NoError(t, f.DeleteDataValidation("Sheet1", "B1")) assert.NoError(t, f.DeleteDataValidation("Sheet1", "A1")) - dvRange.Sqref = "C2:C5" - assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + dv.Sqref = "C2:C5" + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) assert.NoError(t, f.DeleteDataValidation("Sheet1", "C4")) - dvRange = NewDataValidation(true) - dvRange.Sqref = "D2:D2 D3 D4" - assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween)) - dvRange.SetInput("input title", "input body") - assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + dv = NewDataValidation(true) + dv.Sqref = "D2:D2 D3 D4" + assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween)) + dv.SetInput("input title", "input body") + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) assert.NoError(t, f.DeleteDataValidation("Sheet1", "D3")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteDataValidation.xlsx"))) - dvRange.Sqref = "A" - assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + dv.Sqref = "A" + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1:A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) From dc3bf331d541511a7945b4f3b8eb3e1c98904374 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 4 Mar 2023 00:07:04 +0800 Subject: [PATCH 168/213] Breaking change: changed the third parameter for the `AddFilter` - Support to add multiple filter columns - Remove the exported type `AutoFilterListOptions` - Support to specify if show header row of the table - Update unit tests and documents of the function --- adjust.go | 2 +- col_test.go | 2 +- rows_test.go | 2 +- table.go | 98 ++++++++++++++++++++++++++++----------------------- table_test.go | 60 ++++++++++++++++--------------- xmlTable.go | 12 ++----- 6 files changed, 91 insertions(+), 85 deletions(-) diff --git a/adjust.go b/adjust.go index 95832c2be6..b6e16e74aa 100644 --- a/adjust.go +++ b/adjust.go @@ -252,7 +252,7 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, if t.AutoFilter != nil { t.AutoFilter.Ref = t.Ref } - _, _ = f.setTableHeader(sheet, x1, y1, x2) + _, _ = f.setTableHeader(sheet, true, x1, y1, x2) table, _ := xml.Marshal(t) f.saveFileList(tableXML, table) } diff --git a/col_test.go b/col_test.go index 8e15bebc76..0e686a958d 100644 --- a/col_test.go +++ b/col_test.go @@ -412,7 +412,7 @@ func TestInsertCols(t *testing.T) { assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.NoError(t, f.MergeCell(sheet1, "A1", "C3")) - assert.NoError(t, f.AutoFilter(sheet1, "A2:B2", &AutoFilterOptions{Column: "B", Expression: "x != blanks"})) + assert.NoError(t, f.AutoFilter(sheet1, "A2:B2", []AutoFilterOptions{{Column: "B", Expression: "x != blanks"}})) assert.NoError(t, f.InsertCols(sheet1, "A", 1)) // Test insert column with illegal cell reference diff --git a/rows_test.go b/rows_test.go index 8b5008f906..5de8d397e6 100644 --- a/rows_test.go +++ b/rows_test.go @@ -320,7 +320,7 @@ func TestRemoveRow(t *testing.T) { t.FailNow() } - err = f.AutoFilter(sheet1, "A2:A2", &AutoFilterOptions{Column: "A", Expression: "x != blanks"}) + err = f.AutoFilter(sheet1, "A2:A2", []AutoFilterOptions{{Column: "A", Expression: "x != blanks"}}) if !assert.NoError(t, err) { t.FailNow() } diff --git a/table.go b/table.go index a00b0a2006..60cfb9ae93 100644 --- a/table.go +++ b/table.go @@ -131,7 +131,7 @@ func (f *File) addSheetTable(sheet string, rID int) error { // setTableHeader provides a function to set cells value in header row for the // table. -func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn, error) { +func (f *File) setTableHeader(sheet string, showHeaderRow bool, x1, y1, x2 int) ([]*xlsxTableColumn, error) { var ( tableColumns []*xlsxTableColumn idx int @@ -144,11 +144,15 @@ func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn, } name, _ := f.GetCellValue(sheet, cell) if _, err := strconv.Atoi(name); err == nil { - _ = f.SetCellStr(sheet, cell, name) + if showHeaderRow { + _ = f.SetCellStr(sheet, cell, name) + } } if name == "" { name = "Column" + strconv.Itoa(idx) - _ = f.SetCellStr(sheet, cell, name) + if showHeaderRow { + _ = f.SetCellStr(sheet, cell, name) + } } tableColumns = append(tableColumns, &xlsxTableColumn{ ID: idx, @@ -188,13 +192,16 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab if y1 == y2 { y2++ } - + hideHeaderRow := opts != nil && opts.ShowHeaderRow != nil && !*opts.ShowHeaderRow + if hideHeaderRow { + y1++ + } // Correct table range reference, such correct C1:B3 to B1:C3. ref, err := f.coordinatesToRangeRef([]int{x1, y1, x2, y2}) if err != nil { return err } - tableColumns, _ := f.setTableHeader(sheet, x1, y1, x2) + tableColumns, _ := f.setTableHeader(sheet, !hideHeaderRow, x1, y1, x2) name := opts.Name if name == "" { name = "Table" + strconv.Itoa(i) @@ -220,6 +227,10 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab ShowColumnStripes: opts.ShowColumnStripes, }, } + if hideHeaderRow { + t.AutoFilter = nil + t.HeaderRowCount = intPtr(0) + } table, _ := xml.Marshal(t) f.saveFileList(tableXML, table) return nil @@ -230,12 +241,12 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab // way of filtering a 2D range of data based on some simple criteria. For // example applying an auto filter to a cell range A1:D4 in the Sheet1: // -// err := f.AutoFilter("Sheet1", "A1:D4", nil) +// err := f.AutoFilter("Sheet1", "A1:D4", []excelize.AutoFilterOptions{}) // // Filter data in an auto filter: // -// err := f.AutoFilter("Sheet1", "A1:D4", &excelize.AutoFilterOptions{ -// Column: "B", Expression: "x != blanks", +// err := f.AutoFilter("Sheet1", "A1:D4", []excelize.AutoFilterOptions{ +// {Column: "B", Expression: "x != blanks"}, // }) // // Column defines the filter columns in an auto filter range based on simple @@ -296,7 +307,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab // x < 2000 // col < 2000 // Price < 2000 -func (f *File) AutoFilter(sheet, rangeRef string, opts *AutoFilterOptions) error { +func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) error { coordinates, err := rangeRefToCoordinates(rangeRef) if err != nil { return err @@ -343,7 +354,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts *AutoFilterOptions) error // autoFilter provides a function to extract the tokens from the filter // expression. The tokens are mainly non-whitespace groups. -func (f *File) autoFilter(sheet, ref string, columns, col int, opts *AutoFilterOptions) error { +func (f *File) autoFilter(sheet, ref string, columns, col int, opts []AutoFilterOptions) error { ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -356,66 +367,65 @@ func (f *File) autoFilter(sheet, ref string, columns, col int, opts *AutoFilterO Ref: ref, } ws.AutoFilter = filter - if opts == nil || opts.Column == "" || opts.Expression == "" { - return nil - } - - fsCol, err := ColumnNameToNumber(opts.Column) - if err != nil { - return err - } - offset := fsCol - col - if offset < 0 || offset > columns { - return fmt.Errorf("incorrect index of column '%s'", opts.Column) - } - - filter.FilterColumn = append(filter.FilterColumn, &xlsxFilterColumn{ - ColID: offset, - }) - re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`) - token := re.FindAllString(opts.Expression, -1) - if len(token) != 3 && len(token) != 7 { - return fmt.Errorf("incorrect number of tokens in criteria '%s'", opts.Expression) - } - expressions, tokens, err := f.parseFilterExpression(opts.Expression, token) - if err != nil { - return err + for _, opt := range opts { + if opt.Column == "" || opt.Expression == "" { + continue + } + fsCol, err := ColumnNameToNumber(opt.Column) + if err != nil { + return err + } + offset := fsCol - col + if offset < 0 || offset > columns { + return fmt.Errorf("incorrect index of column '%s'", opt.Column) + } + fc := &xlsxFilterColumn{ColID: offset} + re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`) + token := re.FindAllString(opt.Expression, -1) + if len(token) != 3 && len(token) != 7 { + return fmt.Errorf("incorrect number of tokens in criteria '%s'", opt.Expression) + } + expressions, tokens, err := f.parseFilterExpression(opt.Expression, token) + if err != nil { + return err + } + f.writeAutoFilter(fc, expressions, tokens) + filter.FilterColumn = append(filter.FilterColumn, fc) } - f.writeAutoFilter(filter, expressions, tokens) ws.AutoFilter = filter return nil } // writeAutoFilter provides a function to check for single or double custom // filters as default filters and handle them accordingly. -func (f *File) writeAutoFilter(filter *xlsxAutoFilter, exp []int, tokens []string) { +func (f *File) writeAutoFilter(fc *xlsxFilterColumn, exp []int, tokens []string) { if len(exp) == 1 && exp[0] == 2 { // Single equality. var filters []*xlsxFilter filters = append(filters, &xlsxFilter{Val: tokens[0]}) - filter.FilterColumn[0].Filters = &xlsxFilters{Filter: filters} + fc.Filters = &xlsxFilters{Filter: filters} } else if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 { // Double equality with "or" operator. var filters []*xlsxFilter for _, v := range tokens { filters = append(filters, &xlsxFilter{Val: v}) } - filter.FilterColumn[0].Filters = &xlsxFilters{Filter: filters} + fc.Filters = &xlsxFilters{Filter: filters} } else { // Non default custom filter. expRel := map[int]int{0: 0, 1: 2} andRel := map[int]bool{0: true, 1: false} for k, v := range tokens { - f.writeCustomFilter(filter, exp[expRel[k]], v) + f.writeCustomFilter(fc, exp[expRel[k]], v) if k == 1 { - filter.FilterColumn[0].CustomFilters.And = andRel[exp[k]] + fc.CustomFilters.And = andRel[exp[k]] } } } } // writeCustomFilter provides a function to write the element. -func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val string) { +func (f *File) writeCustomFilter(fc *xlsxFilterColumn, operator int, val string) { operators := map[int]string{ 1: "lessThan", 2: "equal", @@ -429,12 +439,12 @@ func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val strin Operator: operators[operator], Val: val, } - if filter.FilterColumn[0].CustomFilters != nil { - filter.FilterColumn[0].CustomFilters.CustomFilter = append(filter.FilterColumn[0].CustomFilters.CustomFilter, &customFilter) + if fc.CustomFilters != nil { + fc.CustomFilters.CustomFilter = append(fc.CustomFilters.CustomFilter, &customFilter) } else { var customFilters []*xlsxCustomFilter customFilters = append(customFilters, &customFilter) - filter.FilterColumn[0].CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters} + fc.CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters} } } diff --git a/table_test.go b/table_test.go index 33ce2e9033..f55a5a0ce7 100644 --- a/table_test.go +++ b/table_test.go @@ -16,12 +16,14 @@ func TestAddTable(t *testing.T) { assert.NoError(t, f.AddTable("Sheet2", "A2:B5", &TableOptions{ Name: "table", StyleName: "TableStyleMedium2", + ShowColumnStripes: true, ShowFirstColumn: true, ShowLastColumn: true, ShowRowStripes: boolPtr(true), - ShowColumnStripes: true, - }, - )) + })) + assert.NoError(t, f.AddTable("Sheet2", "D1:D11", &TableOptions{ + ShowHeaderRow: boolPtr(false), + })) assert.NoError(t, f.AddTable("Sheet2", "F1:F1", &TableOptions{StyleName: "TableStyleMedium8"})) // Test add table in not exist worksheet @@ -60,7 +62,7 @@ func TestAddTable(t *testing.T) { func TestSetTableHeader(t *testing.T) { f := NewFile() - _, err := f.setTableHeader("Sheet1", 1, 0, 1) + _, err := f.setTableHeader("Sheet1", true, 1, 0, 1) assert.EqualError(t, err, "invalid cell reference [1, 0]") } @@ -68,16 +70,16 @@ func TestAutoFilter(t *testing.T) { outFile := filepath.Join("test", "TestAutoFilter%d.xlsx") f, err := prepareTestBook1() assert.NoError(t, err) - for i, opts := range []*AutoFilterOptions{ - nil, - {Column: "B", Expression: ""}, - {Column: "B", Expression: "x != blanks"}, - {Column: "B", Expression: "x == blanks"}, - {Column: "B", Expression: "x != nonblanks"}, - {Column: "B", Expression: "x == nonblanks"}, - {Column: "B", Expression: "x <= 1 and x >= 2"}, - {Column: "B", Expression: "x == 1 or x == 2"}, - {Column: "B", Expression: "x == 1 or x == 2*"}, + for i, opts := range [][]AutoFilterOptions{ + {}, + {{Column: "B", Expression: ""}}, + {{Column: "B", Expression: "x != blanks"}}, + {{Column: "B", Expression: "x == blanks"}}, + {{Column: "B", Expression: "x != nonblanks"}}, + {{Column: "B", Expression: "x == nonblanks"}}, + {{Column: "B", Expression: "x <= 1 and x >= 2"}}, + {{Column: "B", Expression: "x == 1 or x == 2"}}, + {{Column: "B", Expression: "x == 1 or x == 2*"}}, } { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { assert.NoError(t, f.AutoFilter("Sheet1", "D4:B1", opts)) @@ -100,13 +102,13 @@ func TestAutoFilterError(t *testing.T) { outFile := filepath.Join("test", "TestAutoFilterError%d.xlsx") f, err := prepareTestBook1() assert.NoError(t, err) - for i, opts := range []*AutoFilterOptions{ - {Column: "B", Expression: "x <= 1 and x >= blanks"}, - {Column: "B", Expression: "x -- y or x == *2*"}, - {Column: "B", Expression: "x != y or x ? *2"}, - {Column: "B", Expression: "x -- y o r x == *2"}, - {Column: "B", Expression: "x -- y"}, - {Column: "A", Expression: "x -- y"}, + for i, opts := range [][]AutoFilterOptions{ + {{Column: "B", Expression: "x <= 1 and x >= blanks"}}, + {{Column: "B", Expression: "x -- y or x == *2*"}}, + {{Column: "B", Expression: "x != y or x ? *2"}}, + {{Column: "B", Expression: "x -- y o r x == *2"}}, + {{Column: "B", Expression: "x -- y"}}, + {{Column: "A", Expression: "x -- y"}}, } { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { if assert.Error(t, f.AutoFilter("Sheet2", "D4:B1", opts)) { @@ -115,22 +117,22 @@ func TestAutoFilterError(t *testing.T) { }) } - assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, &AutoFilterOptions{ + assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, []AutoFilterOptions{{ Column: "A", Expression: "", - }), "sheet SheetN does not exist") - assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &AutoFilterOptions{ + }}), "sheet SheetN does not exist") + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{ Column: "-", Expression: "-", - }), newInvalidColumnNameError("-").Error()) - assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, &AutoFilterOptions{ + }}), newInvalidColumnNameError("-").Error()) + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, []AutoFilterOptions{{ Column: "A", Expression: "-", - }), `incorrect index of column 'A'`) - assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &AutoFilterOptions{ + }}), `incorrect index of column 'A'`) + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{ Column: "A", Expression: "-", - }), `incorrect number of tokens in criteria '-'`) + }}), `incorrect number of tokens in criteria '-'`) } func TestParseFilterTokens(t *testing.T) { diff --git a/xmlTable.go b/xmlTable.go index 5710bc06d9..0779a8e004 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -26,7 +26,7 @@ type xlsxTable struct { DisplayName string `xml:"displayName,attr,omitempty"` HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"` HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"` - HeaderRowCount int `xml:"headerRowCount,attr,omitempty"` + HeaderRowCount *int `xml:"headerRowCount,attr"` HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"` ID int `xml:"id,attr"` InsertRow bool `xml:"insertRow,attr,omitempty"` @@ -200,21 +200,15 @@ type xlsxTableStyleInfo struct { type TableOptions struct { Name string StyleName string + ShowColumnStripes bool ShowFirstColumn bool + ShowHeaderRow *bool ShowLastColumn bool ShowRowStripes *bool - ShowColumnStripes bool -} - -// AutoFilterListOptions directly maps the auto filter list settings. -type AutoFilterListOptions struct { - Column string - Value []int } // AutoFilterOptions directly maps the auto filter settings. type AutoFilterOptions struct { Column string Expression string - FilterList []AutoFilterListOptions } From 0d193c76ac49eebade9802434b9288264176c072 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 14 Mar 2023 00:58:20 +0800 Subject: [PATCH 169/213] This closes #1492, fix data bar min/max value doesn't work --- styles.go | 4 +++- styles_test.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/styles.go b/styles.go index 3c02e2d38c..7c679c26c5 100644 --- a/styles.go +++ b/styles.go @@ -3457,7 +3457,9 @@ func extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatO if c.DataBar != nil { format.StopIfTrue = c.StopIfTrue format.MinType = c.DataBar.Cfvo[0].Type + format.MinValue = c.DataBar.Cfvo[0].Val format.MaxType = c.DataBar.Cfvo[1].Type + format.MaxValue = c.DataBar.Cfvo[1].Val format.BarColor = "#" + strings.TrimPrefix(strings.ToUpper(c.DataBar.Color[0].RGB), "FF") if c.DataBar.ShowValue != nil { format.BarOnly = !*c.DataBar.ShowValue @@ -3707,7 +3709,7 @@ func drawCondFmtDataBar(p int, ct, GUID string, format *ConditionalFormatOptions Type: validType[format.Type], DataBar: &xlsxDataBar{ ShowValue: boolPtr(!format.BarOnly), - Cfvo: []*xlsxCfvo{{Type: format.MinType}, {Type: format.MaxType}}, + Cfvo: []*xlsxCfvo{{Type: format.MinType, Val: format.MinValue}, {Type: format.MaxType, Val: format.MaxValue}}, Color: []*xlsxColor{{RGB: getPaletteColor(format.BarColor)}}, }, ExtLst: extLst, diff --git a/styles_test.go b/styles_test.go index 257547a64c..f8ca15e035 100644 --- a/styles_test.go +++ b/styles_test.go @@ -205,7 +205,7 @@ func TestGetConditionalFormats(t *testing.T) { {{Type: "unique", Format: 1, Criteria: "="}}, {{Type: "3_color_scale", Criteria: "=", MinType: "num", MidType: "num", MaxType: "num", MinValue: "-10", MidValue: "50", MaxValue: "10", MinColor: "#FF0000", MidColor: "#00FF00", MaxColor: "#0000FF"}}, {{Type: "2_color_scale", Criteria: "=", MinType: "num", MaxType: "num", MinColor: "#FF0000", MaxColor: "#0000FF"}}, - {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarOnly: true, BarSolid: true, StopIfTrue: true}}, + {{Type: "data_bar", Criteria: "=", MinType: "num", MaxType: "num", MinValue: "-10", MaxValue: "10", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarOnly: true, BarSolid: true, StopIfTrue: true}}, {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarDirection: "rightToLeft", BarOnly: true, BarSolid: true, StopIfTrue: true}}, {{Type: "formula", Format: 1, Criteria: "="}}, {{Type: "icon_set", IconStyle: "3Arrows", ReverseIcons: true, IconsOnly: true}}, From e394f01a975247b0ebe6f014ff81257f7628136e Mon Sep 17 00:00:00 2001 From: Rizki Putra Date: Wed, 15 Mar 2023 08:17:30 +0700 Subject: [PATCH 170/213] This update the return value for the `CalcCellValue` function (#1490) - Using formula error string in the result of the `CalcCellValue` function - Using the error message in the `CalcCellValue` function returns error - Update unit tests --- calc.go | 15 +- calc_test.go | 4245 +++++++++++++++++++++++++------------------------- 2 files changed, 2131 insertions(+), 2129 deletions(-) diff --git a/calc.go b/calc.go index 7b8dcf525d..47705caad1 100644 --- a/calc.go +++ b/calc.go @@ -782,6 +782,7 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string iterations: make(map[string]uint), iterationsCache: make(map[string]formulaArg), }, sheet, cell); err != nil { + result = token.String return } if !rawCellValue { @@ -1002,8 +1003,8 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T inArray = false continue } - if err = f.evalInfixExpFunc(ctx, sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { - return newEmptyFormulaArg(), err + if errArg := f.evalInfixExpFunc(ctx, sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); errArg.Type == ArgError { + return errArg, errors.New(errArg.Error) } } } @@ -1021,9 +1022,9 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T } // evalInfixExpFunc evaluate formula function in the infix expression. -func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nextToken efp.Token, opfStack, opdStack, opftStack, opfdStack, argsStack *Stack) error { +func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nextToken efp.Token, opfStack, opdStack, opftStack, opfdStack, argsStack *Stack) formulaArg { if !isFunctionStopToken(token) { - return nil + return newEmptyFormulaArg() } prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack) // call formula function to evaluate @@ -1031,7 +1032,7 @@ func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nex "_xlfn.", "", ".", "dot").Replace(opfStack.Peek().(efp.Token).TValue), []reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))}) if arg.Type == ArgError && opfStack.Len() == 1 { - return errors.New(arg.Value()) + return arg } argsStack.Pop() opftStack.Pop() // remove current function separator @@ -1050,7 +1051,7 @@ func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nex } opdStack.Push(newStringFormulaArg(val)) } - return nil + return newEmptyFormulaArg() } // prepareEvalInfixExp check the token and stack state for formula function @@ -11612,7 +11613,7 @@ func (fn *formulaFuncs) IFNA(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "IFNA requires 2 arguments") } arg := argsList.Front().Value.(formulaArg) - if arg.Type == ArgError && arg.Value() == formulaErrorNA { + if arg.Type == ArgError && arg.String == formulaErrorNA { return argsList.Back().Value.(formulaArg) } return arg diff --git a/calc_test.go b/calc_test.go index 8c0ac2e369..1cb1580d08 100644 --- a/calc_test.go +++ b/calc_test.go @@ -64,8 +64,8 @@ func TestCalcCellValue(t *testing.T) { "={1}+2": "3", "=1+{2}": "3", "={1}+{2}": "3", - `="A"="A"`: "TRUE", - `="A"<>"A"`: "FALSE", + "=\"A\"=\"A\"": "TRUE", + "=\"A\"<>\"A\"": "FALSE", // Engineering Functions // BESSELI "=BESSELI(4.5,1)": "15.3892227537359", @@ -531,11 +531,11 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.CSCH(-3.14159265358979)": "-0.0865895375300472", "=_xlfn.CSCH(_xlfn.CSCH(1))": "1.04451010395518", // _xlfn.DECIMAL - `=_xlfn.DECIMAL("1100",2)`: "12", - `=_xlfn.DECIMAL("186A0",16)`: "100000", - `=_xlfn.DECIMAL("31L0",32)`: "100000", - `=_xlfn.DECIMAL("70122",8)`: "28754", - `=_xlfn.DECIMAL("0x70122",8)`: "28754", + "=_xlfn.DECIMAL(\"1100\",2)": "12", + "=_xlfn.DECIMAL(\"186A0\",16)": "100000", + "=_xlfn.DECIMAL(\"31L0\",32)": "100000", + "=_xlfn.DECIMAL(\"70122\",8)": "28754", + "=_xlfn.DECIMAL(\"0x70122\",8)": "28754", // DEGREES "=DEGREES(1)": "57.2957795130823", "=DEGREES(2.5)": "143.239448782706", @@ -628,9 +628,9 @@ func TestCalcCellValue(t *testing.T) { "=LCM(1,8,12)": "24", "=LCM(7,2)": "14", "=LCM(7)": "7", - `=LCM("",1)`: "1", - `=LCM(0,0)`: "0", - `=LCM(0,LCM(0,0))`: "0", + "=LCM(\"\",1)": "1", + "=LCM(0,0)": "0", + "=LCM(0,LCM(0,0))": "0", // LN "=LN(1)": "0", "=LN(100)": "4.60517018598809", @@ -661,7 +661,7 @@ func TestCalcCellValue(t *testing.T) { "=IMPOWER(\"2+4i\",-2)": "-0.03-0.04i", // IMPRODUCT "=IMPRODUCT(3,6)": "18", - `=IMPRODUCT("",3,SUM(6))`: "18", + "=IMPRODUCT(\"\",3,SUM(6))": "18", "=IMPRODUCT(\"1-i\",\"5+10i\",2)": "30+10i", "=IMPRODUCT(COMPLEX(5,2),COMPLEX(0,1))": "-2+5i", "=IMPRODUCT(A1:C1)": "4", @@ -688,7 +688,7 @@ func TestCalcCellValue(t *testing.T) { "=MROUND(MROUND(1,1),1)": "1", // MULTINOMIAL "=MULTINOMIAL(3,1,2,5)": "27720", - `=MULTINOMIAL("",3,1,2,5)`: "27720", + "=MULTINOMIAL(\"\",3,1,2,5)": "27720", "=MULTINOMIAL(MULTINOMIAL(1))": "1", // _xlfn.MUNIT "=_xlfn.MUNIT(4)": "1", @@ -751,7 +751,7 @@ func TestCalcCellValue(t *testing.T) { "=ROUNDDOWN(-99.999,2)": "-99.99", "=ROUNDDOWN(-99.999,-1)": "-90", "=ROUNDDOWN(ROUNDDOWN(100,1),-1)": "100", - // ROUNDUP` + // ROUNDUP "=ROUNDUP(11.111,1)": "11.2", "=ROUNDUP(11.111,2)": "11.12", "=ROUNDUP(11.111,0)": "12", @@ -856,20 +856,20 @@ func TestCalcCellValue(t *testing.T) { "=SUM((SUM(2))+1)": "3", "=SUM({1,2,3,4,\"\"})": "10", // SUMIF - `=SUMIF(F1:F5, "")`: "0", - `=SUMIF(A1:A5, "3")`: "3", - `=SUMIF(F1:F5, "=36693")`: "36693", - `=SUMIF(F1:F5, "<100")`: "0", - `=SUMIF(F1:F5, "<=36693")`: "93233", - `=SUMIF(F1:F5, ">100")`: "146554", - `=SUMIF(F1:F5, ">=100")`: "146554", - `=SUMIF(F1:F5, ">=text")`: "0", - `=SUMIF(F1:F5, "*Jan",F2:F5)`: "0", - `=SUMIF(D3:D7,"Jan",F2:F5)`: "112114", - `=SUMIF(D2:D9,"Feb",F2:F9)`: "157559", - `=SUMIF(E2:E9,"North 1",F2:F9)`: "66582", - `=SUMIF(E2:E9,"North*",F2:F9)`: "138772", - "=SUMIF(D1:D3,\"Month\",D1:D3)": "0", + "=SUMIF(F1:F5, \"\")": "0", + "=SUMIF(A1:A5, \"3\")": "3", + "=SUMIF(F1:F5, \"=36693\")": "36693", + "=SUMIF(F1:F5, \"<100\")": "0", + "=SUMIF(F1:F5, \"<=36693\")": "93233", + "=SUMIF(F1:F5, \">100\")": "146554", + "=SUMIF(F1:F5, \">=100\")": "146554", + "=SUMIF(F1:F5, \">=text\")": "0", + "=SUMIF(F1:F5, \"*Jan\",F2:F5)": "0", + "=SUMIF(D3:D7,\"Jan\",F2:F5)": "112114", + "=SUMIF(D2:D9,\"Feb\",F2:F9)": "157559", + "=SUMIF(E2:E9,\"North 1\",F2:F9)": "66582", + "=SUMIF(E2:E9,\"North*\",F2:F9)": "138772", + "=SUMIF(D1:D3,\"Month\",D1:D3)": "0", // SUMPRODUCT "=SUMPRODUCT(A1,B1)": "4", "=SUMPRODUCT(A1:A2,B1:B2)": "14", @@ -1396,10 +1396,10 @@ func TestCalcCellValue(t *testing.T) { "=ISNA(A1)": "FALSE", "=ISNA(NA())": "TRUE", // ISNONTEXT - "=ISNONTEXT(A1)": "TRUE", - "=ISNONTEXT(A5)": "TRUE", - `=ISNONTEXT("Excelize")`: "FALSE", - "=ISNONTEXT(NA())": "TRUE", + "=ISNONTEXT(A1)": "TRUE", + "=ISNONTEXT(A5)": "TRUE", + "=ISNONTEXT(\"Excelize\")": "FALSE", + "=ISNONTEXT(NA())": "TRUE", // ISNUMBER "=ISNUMBER(A1)": "TRUE", "=ISNUMBER(D1)": "FALSE", @@ -1457,8 +1457,9 @@ func TestCalcCellValue(t *testing.T) { "=IFERROR(G1,2)": "0", "=IFERROR(B2/MROUND(A2,1),0)": "2.5", // IFNA - "=IFNA(1,\"not found\")": "1", - "=IFNA(NA(),\"not found\")": "not found", + "=IFNA(1,\"not found\")": "1", + "=IFNA(NA(),\"not found\")": "not found", + "=IFNA(HLOOKUP(D2,D:D,1,2),\"not found\")": "not found", // IFS "=IFS(4>1,5/4,4<-1,-5/4,TRUE,0)": "1.25", "=IFS(-2>1,5/-2,-2<-1,-5/-2,TRUE,0)": "2.5", @@ -2122,2247 +2123,2247 @@ func TestCalcCellValue(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - mathCalcError := map[string]string{ - "=1/0": "#DIV/0!", - "1^\"text\"": "strconv.ParseFloat: parsing \"text\": invalid syntax", - "\"text\"^1": "strconv.ParseFloat: parsing \"text\": invalid syntax", - "1+\"text\"": "strconv.ParseFloat: parsing \"text\": invalid syntax", - "\"text\"+1": "strconv.ParseFloat: parsing \"text\": invalid syntax", - "1-\"text\"": "strconv.ParseFloat: parsing \"text\": invalid syntax", - "\"text\"-1": "strconv.ParseFloat: parsing \"text\": invalid syntax", - "1*\"text\"": "strconv.ParseFloat: parsing \"text\": invalid syntax", - "\"text\"*1": "strconv.ParseFloat: parsing \"text\": invalid syntax", - "1/\"text\"": "strconv.ParseFloat: parsing \"text\": invalid syntax", - "\"text\"/1": "strconv.ParseFloat: parsing \"text\": invalid syntax", + mathCalcError := map[string][]string{ + "=1/0": {"", "#DIV/0!"}, + "1^\"text\"": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"}, + "\"text\"^1": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"}, + "1+\"text\"": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"}, + "\"text\"+1": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"}, + "1-\"text\"": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"}, + "\"text\"-1": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"}, + "1*\"text\"": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"}, + "\"text\"*1": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"}, + "1/\"text\"": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"}, + "\"text\"/1": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"}, // Engineering Functions // BESSELI - "=BESSELI()": "BESSELI requires 2 numeric arguments", - "=BESSELI(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BESSELI(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BESSELI()": {"#VALUE!", "BESSELI requires 2 numeric arguments"}, + "=BESSELI(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BESSELI(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // BESSELJ - "=BESSELJ()": "BESSELJ requires 2 numeric arguments", - "=BESSELJ(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BESSELJ(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BESSELJ()": {"#VALUE!", "BESSELJ requires 2 numeric arguments"}, + "=BESSELJ(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BESSELJ(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // BESSELK - "=BESSELK()": "BESSELK requires 2 numeric arguments", - "=BESSELK(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BESSELK(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BESSELK(-1,0)": "#NUM!", - "=BESSELK(1,-1)": "#NUM!", + "=BESSELK()": {"#VALUE!", "BESSELK requires 2 numeric arguments"}, + "=BESSELK(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BESSELK(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BESSELK(-1,0)": {"#NUM!", "#NUM!"}, + "=BESSELK(1,-1)": {"#NUM!", "#NUM!"}, // BESSELY - "=BESSELY()": "BESSELY requires 2 numeric arguments", - "=BESSELY(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BESSELY(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BESSELY(-1,0)": "#NUM!", - "=BESSELY(1,-1)": "#NUM!", + "=BESSELY()": {"#VALUE!", "BESSELY requires 2 numeric arguments"}, + "=BESSELY(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BESSELY(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BESSELY(-1,0)": {"#NUM!", "#NUM!"}, + "=BESSELY(1,-1)": {"#NUM!", "#NUM!"}, // BIN2DEC - "=BIN2DEC()": "BIN2DEC requires 1 numeric argument", - "=BIN2DEC(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BIN2DEC()": {"#VALUE!", "BIN2DEC requires 1 numeric argument"}, + "=BIN2DEC(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // BIN2HEX - "=BIN2HEX()": "BIN2HEX requires at least 1 argument", - "=BIN2HEX(1,1,1)": "BIN2HEX allows at most 2 arguments", - "=BIN2HEX(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BIN2HEX(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BIN2HEX(12345678901,10)": "#NUM!", - "=BIN2HEX(1,-1)": "#NUM!", - "=BIN2HEX(31,1)": "#NUM!", + "=BIN2HEX()": {"#VALUE!", "BIN2HEX requires at least 1 argument"}, + "=BIN2HEX(1,1,1)": {"#VALUE!", "BIN2HEX allows at most 2 arguments"}, + "=BIN2HEX(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BIN2HEX(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BIN2HEX(12345678901,10)": {"#NUM!", "#NUM!"}, + "=BIN2HEX(1,-1)": {"#NUM!", "#NUM!"}, + "=BIN2HEX(31,1)": {"#NUM!", "#NUM!"}, // BIN2OCT - "=BIN2OCT()": "BIN2OCT requires at least 1 argument", - "=BIN2OCT(1,1,1)": "BIN2OCT allows at most 2 arguments", - "=BIN2OCT(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BIN2OCT(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BIN2OCT(-12345678901 ,10)": "#NUM!", - "=BIN2OCT(1,-1)": "#NUM!", - "=BIN2OCT(8,1)": "#NUM!", + "=BIN2OCT()": {"#VALUE!", "BIN2OCT requires at least 1 argument"}, + "=BIN2OCT(1,1,1)": {"#VALUE!", "BIN2OCT allows at most 2 arguments"}, + "=BIN2OCT(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BIN2OCT(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BIN2OCT(-12345678901 ,10)": {"#NUM!", "#NUM!"}, + "=BIN2OCT(1,-1)": {"#NUM!", "#NUM!"}, + "=BIN2OCT(8,1)": {"#NUM!", "#NUM!"}, // BITAND - "=BITAND()": "BITAND requires 2 numeric arguments", - "=BITAND(-1,2)": "#NUM!", - "=BITAND(2^48,2)": "#NUM!", - "=BITAND(1,-1)": "#NUM!", - "=BITAND(\"\",-1)": "#NUM!", - "=BITAND(1,\"\")": "#NUM!", - "=BITAND(1,2^48)": "#NUM!", + "=BITAND()": {"#VALUE!", "BITAND requires 2 numeric arguments"}, + "=BITAND(-1,2)": {"#NUM!", "#NUM!"}, + "=BITAND(2^48,2)": {"#NUM!", "#NUM!"}, + "=BITAND(1,-1)": {"#NUM!", "#NUM!"}, + "=BITAND(\"\",-1)": {"#NUM!", "#NUM!"}, + "=BITAND(1,\"\")": {"#NUM!", "#NUM!"}, + "=BITAND(1,2^48)": {"#NUM!", "#NUM!"}, // BITLSHIFT - "=BITLSHIFT()": "BITLSHIFT requires 2 numeric arguments", - "=BITLSHIFT(-1,2)": "#NUM!", - "=BITLSHIFT(2^48,2)": "#NUM!", - "=BITLSHIFT(1,-1)": "#NUM!", - "=BITLSHIFT(\"\",-1)": "#NUM!", - "=BITLSHIFT(1,\"\")": "#NUM!", - "=BITLSHIFT(1,2^48)": "#NUM!", + "=BITLSHIFT()": {"#VALUE!", "BITLSHIFT requires 2 numeric arguments"}, + "=BITLSHIFT(-1,2)": {"#NUM!", "#NUM!"}, + "=BITLSHIFT(2^48,2)": {"#NUM!", "#NUM!"}, + "=BITLSHIFT(1,-1)": {"#NUM!", "#NUM!"}, + "=BITLSHIFT(\"\",-1)": {"#NUM!", "#NUM!"}, + "=BITLSHIFT(1,\"\")": {"#NUM!", "#NUM!"}, + "=BITLSHIFT(1,2^48)": {"#NUM!", "#NUM!"}, // BITOR - "=BITOR()": "BITOR requires 2 numeric arguments", - "=BITOR(-1,2)": "#NUM!", - "=BITOR(2^48,2)": "#NUM!", - "=BITOR(1,-1)": "#NUM!", - "=BITOR(\"\",-1)": "#NUM!", - "=BITOR(1,\"\")": "#NUM!", - "=BITOR(1,2^48)": "#NUM!", + "=BITOR()": {"#VALUE!", "BITOR requires 2 numeric arguments"}, + "=BITOR(-1,2)": {"#NUM!", "#NUM!"}, + "=BITOR(2^48,2)": {"#NUM!", "#NUM!"}, + "=BITOR(1,-1)": {"#NUM!", "#NUM!"}, + "=BITOR(\"\",-1)": {"#NUM!", "#NUM!"}, + "=BITOR(1,\"\")": {"#NUM!", "#NUM!"}, + "=BITOR(1,2^48)": {"#NUM!", "#NUM!"}, // BITRSHIFT - "=BITRSHIFT()": "BITRSHIFT requires 2 numeric arguments", - "=BITRSHIFT(-1,2)": "#NUM!", - "=BITRSHIFT(2^48,2)": "#NUM!", - "=BITRSHIFT(1,-1)": "#NUM!", - "=BITRSHIFT(\"\",-1)": "#NUM!", - "=BITRSHIFT(1,\"\")": "#NUM!", - "=BITRSHIFT(1,2^48)": "#NUM!", + "=BITRSHIFT()": {"#VALUE!", "BITRSHIFT requires 2 numeric arguments"}, + "=BITRSHIFT(-1,2)": {"#NUM!", "#NUM!"}, + "=BITRSHIFT(2^48,2)": {"#NUM!", "#NUM!"}, + "=BITRSHIFT(1,-1)": {"#NUM!", "#NUM!"}, + "=BITRSHIFT(\"\",-1)": {"#NUM!", "#NUM!"}, + "=BITRSHIFT(1,\"\")": {"#NUM!", "#NUM!"}, + "=BITRSHIFT(1,2^48)": {"#NUM!", "#NUM!"}, // BITXOR - "=BITXOR()": "BITXOR requires 2 numeric arguments", - "=BITXOR(-1,2)": "#NUM!", - "=BITXOR(2^48,2)": "#NUM!", - "=BITXOR(1,-1)": "#NUM!", - "=BITXOR(\"\",-1)": "#NUM!", - "=BITXOR(1,\"\")": "#NUM!", - "=BITXOR(1,2^48)": "#NUM!", + "=BITXOR()": {"#VALUE!", "BITXOR requires 2 numeric arguments"}, + "=BITXOR(-1,2)": {"#NUM!", "#NUM!"}, + "=BITXOR(2^48,2)": {"#NUM!", "#NUM!"}, + "=BITXOR(1,-1)": {"#NUM!", "#NUM!"}, + "=BITXOR(\"\",-1)": {"#NUM!", "#NUM!"}, + "=BITXOR(1,\"\")": {"#NUM!", "#NUM!"}, + "=BITXOR(1,2^48)": {"#NUM!", "#NUM!"}, // COMPLEX - "=COMPLEX()": "COMPLEX requires at least 2 arguments", - "=COMPLEX(10,-5,\"\")": "#VALUE!", - "=COMPLEX(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=COMPLEX(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=COMPLEX(10,-5,\"i\",0)": "COMPLEX allows at most 3 arguments", + "=COMPLEX()": {"#VALUE!", "COMPLEX requires at least 2 arguments"}, + "=COMPLEX(10,-5,\"\")": {"#VALUE!", "#VALUE!"}, + "=COMPLEX(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=COMPLEX(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=COMPLEX(10,-5,\"i\",0)": {"#VALUE!", "COMPLEX allows at most 3 arguments"}, // CONVERT - "=CONVERT()": "CONVERT requires 3 arguments", - "=CONVERT(\"\",\"m\",\"yd\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CONVERT(20.2,\"m\",\"C\")": "#N/A", - "=CONVERT(20.2,\"\",\"C\")": "#N/A", - "=CONVERT(100,\"dapt\",\"pt\")": "#N/A", - "=CONVERT(1,\"ft\",\"day\")": "#N/A", - "=CONVERT(234.56,\"kpt\",\"lt\")": "#N/A", - "=CONVERT(234.56,\"lt\",\"kpt\")": "#N/A", - "=CONVERT(234.56,\"kiqt\",\"pt\")": "#N/A", - "=CONVERT(234.56,\"pt\",\"kiqt\")": "#N/A", - "=CONVERT(12345.6,\"baton\",\"cwt\")": "#N/A", - "=CONVERT(12345.6,\"cwt\",\"baton\")": "#N/A", - "=CONVERT(234.56,\"xxxx\",\"m\")": "#N/A", - "=CONVERT(234.56,\"m\",\"xxxx\")": "#N/A", + "=CONVERT()": {"#VALUE!", "CONVERT requires 3 arguments"}, + "=CONVERT(\"\",\"m\",\"yd\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CONVERT(20.2,\"m\",\"C\")": {"#N/A", "#N/A"}, + "=CONVERT(20.2,\"\",\"C\")": {"#N/A", "#N/A"}, + "=CONVERT(100,\"dapt\",\"pt\")": {"#N/A", "#N/A"}, + "=CONVERT(1,\"ft\",\"day\")": {"#N/A", "#N/A"}, + "=CONVERT(234.56,\"kpt\",\"lt\")": {"#N/A", "#N/A"}, + "=CONVERT(234.56,\"lt\",\"kpt\")": {"#N/A", "#N/A"}, + "=CONVERT(234.56,\"kiqt\",\"pt\")": {"#N/A", "#N/A"}, + "=CONVERT(234.56,\"pt\",\"kiqt\")": {"#N/A", "#N/A"}, + "=CONVERT(12345.6,\"baton\",\"cwt\")": {"#N/A", "#N/A"}, + "=CONVERT(12345.6,\"cwt\",\"baton\")": {"#N/A", "#N/A"}, + "=CONVERT(234.56,\"xxxx\",\"m\")": {"#N/A", "#N/A"}, + "=CONVERT(234.56,\"m\",\"xxxx\")": {"#N/A", "#N/A"}, // DEC2BIN - "=DEC2BIN()": "DEC2BIN requires at least 1 argument", - "=DEC2BIN(1,1,1)": "DEC2BIN allows at most 2 arguments", - "=DEC2BIN(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DEC2BIN(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DEC2BIN(-513,10)": "#NUM!", - "=DEC2BIN(1,-1)": "#NUM!", - "=DEC2BIN(2,1)": "#NUM!", + "=DEC2BIN()": {"#VALUE!", "DEC2BIN requires at least 1 argument"}, + "=DEC2BIN(1,1,1)": {"#VALUE!", "DEC2BIN allows at most 2 arguments"}, + "=DEC2BIN(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DEC2BIN(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DEC2BIN(-513,10)": {"#NUM!", "#NUM!"}, + "=DEC2BIN(1,-1)": {"#NUM!", "#NUM!"}, + "=DEC2BIN(2,1)": {"#NUM!", "#NUM!"}, // DEC2HEX - "=DEC2HEX()": "DEC2HEX requires at least 1 argument", - "=DEC2HEX(1,1,1)": "DEC2HEX allows at most 2 arguments", - "=DEC2HEX(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DEC2HEX(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DEC2HEX(-549755813888,10)": "#NUM!", - "=DEC2HEX(1,-1)": "#NUM!", - "=DEC2HEX(31,1)": "#NUM!", + "=DEC2HEX()": {"#VALUE!", "DEC2HEX requires at least 1 argument"}, + "=DEC2HEX(1,1,1)": {"#VALUE!", "DEC2HEX allows at most 2 arguments"}, + "=DEC2HEX(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DEC2HEX(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DEC2HEX(-549755813888,10)": {"#NUM!", "#NUM!"}, + "=DEC2HEX(1,-1)": {"#NUM!", "#NUM!"}, + "=DEC2HEX(31,1)": {"#NUM!", "#NUM!"}, // DEC2OCT - "=DEC2OCT()": "DEC2OCT requires at least 1 argument", - "=DEC2OCT(1,1,1)": "DEC2OCT allows at most 2 arguments", - "=DEC2OCT(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DEC2OCT(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DEC2OCT(-536870912 ,10)": "#NUM!", - "=DEC2OCT(1,-1)": "#NUM!", - "=DEC2OCT(8,1)": "#NUM!", + "=DEC2OCT()": {"#VALUE!", "DEC2OCT requires at least 1 argument"}, + "=DEC2OCT(1,1,1)": {"#VALUE!", "DEC2OCT allows at most 2 arguments"}, + "=DEC2OCT(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DEC2OCT(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DEC2OCT(-536870912 ,10)": {"#NUM!", "#NUM!"}, + "=DEC2OCT(1,-1)": {"#NUM!", "#NUM!"}, + "=DEC2OCT(8,1)": {"#NUM!", "#NUM!"}, // DELTA - "=DELTA()": "DELTA requires at least 1 argument", - "=DELTA(0,0,0)": "DELTA allows at most 2 arguments", - "=DELTA(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DELTA(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=DELTA()": {"#VALUE!", "DELTA requires at least 1 argument"}, + "=DELTA(0,0,0)": {"#VALUE!", "DELTA allows at most 2 arguments"}, + "=DELTA(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DELTA(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // ERF - "=ERF()": "ERF requires at least 1 argument", - "=ERF(0,0,0)": "ERF allows at most 2 arguments", - "=ERF(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=ERF(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=ERF()": {"#VALUE!", "ERF requires at least 1 argument"}, + "=ERF(0,0,0)": {"#VALUE!", "ERF allows at most 2 arguments"}, + "=ERF(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ERF(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // ERF.PRECISE - "=ERF.PRECISE()": "ERF.PRECISE requires 1 argument", - "=ERF.PRECISE(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=ERF.PRECISE()": {"#VALUE!", "ERF.PRECISE requires 1 argument"}, + "=ERF.PRECISE(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // ERFC - "=ERFC()": "ERFC requires 1 argument", - "=ERFC(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=ERFC()": {"#VALUE!", "ERFC requires 1 argument"}, + "=ERFC(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // ERFC.PRECISE - "=ERFC.PRECISE()": "ERFC.PRECISE requires 1 argument", - "=ERFC.PRECISE(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=ERFC.PRECISE()": {"#VALUE!", "ERFC.PRECISE requires 1 argument"}, + "=ERFC.PRECISE(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // GESTEP - "=GESTEP()": "GESTEP requires at least 1 argument", - "=GESTEP(0,0,0)": "GESTEP allows at most 2 arguments", - "=GESTEP(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GESTEP(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=GESTEP()": {"#VALUE!", "GESTEP requires at least 1 argument"}, + "=GESTEP(0,0,0)": {"#VALUE!", "GESTEP allows at most 2 arguments"}, + "=GESTEP(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GESTEP(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // HEX2BIN - "=HEX2BIN()": "HEX2BIN requires at least 1 argument", - "=HEX2BIN(1,1,1)": "HEX2BIN allows at most 2 arguments", - "=HEX2BIN(\"X\",1)": "strconv.ParseInt: parsing \"X\": invalid syntax", - "=HEX2BIN(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=HEX2BIN(-513,10)": "strconv.ParseInt: parsing \"-\": invalid syntax", - "=HEX2BIN(1,-1)": "#NUM!", - "=HEX2BIN(2,1)": "#NUM!", + "=HEX2BIN()": {"#VALUE!", "HEX2BIN requires at least 1 argument"}, + "=HEX2BIN(1,1,1)": {"#VALUE!", "HEX2BIN allows at most 2 arguments"}, + "=HEX2BIN(\"X\",1)": {"#NUM!", "strconv.ParseInt: parsing \"X\": invalid syntax"}, + "=HEX2BIN(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=HEX2BIN(-513,10)": {"#NUM!", "strconv.ParseInt: parsing \"-\": invalid syntax"}, + "=HEX2BIN(1,-1)": {"#NUM!", "#NUM!"}, + "=HEX2BIN(2,1)": {"#NUM!", "#NUM!"}, // HEX2DEC - "=HEX2DEC()": "HEX2DEC requires 1 numeric argument", - "=HEX2DEC(\"X\")": "strconv.ParseInt: parsing \"X\": invalid syntax", + "=HEX2DEC()": {"#VALUE!", "HEX2DEC requires 1 numeric argument"}, + "=HEX2DEC(\"X\")": {"#NUM!", "strconv.ParseInt: parsing \"X\": invalid syntax"}, // HEX2OCT - "=HEX2OCT()": "HEX2OCT requires at least 1 argument", - "=HEX2OCT(1,1,1)": "HEX2OCT allows at most 2 arguments", - "=HEX2OCT(\"X\",1)": "strconv.ParseInt: parsing \"X\": invalid syntax", - "=HEX2OCT(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=HEX2OCT(-513,10)": "strconv.ParseInt: parsing \"-\": invalid syntax", - "=HEX2OCT(1,-1)": "#NUM!", + "=HEX2OCT()": {"#VALUE!", "HEX2OCT requires at least 1 argument"}, + "=HEX2OCT(1,1,1)": {"#VALUE!", "HEX2OCT allows at most 2 arguments"}, + "=HEX2OCT(\"X\",1)": {"#NUM!", "strconv.ParseInt: parsing \"X\": invalid syntax"}, + "=HEX2OCT(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=HEX2OCT(-513,10)": {"#NUM!", "strconv.ParseInt: parsing \"-\": invalid syntax"}, + "=HEX2OCT(1,-1)": {"#NUM!", "#NUM!"}, // IMABS - "=IMABS()": "IMABS requires 1 argument", - "=IMABS(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMABS()": {"#VALUE!", "IMABS requires 1 argument"}, + "=IMABS(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMAGINARY - "=IMAGINARY()": "IMAGINARY requires 1 argument", - "=IMAGINARY(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMAGINARY()": {"#VALUE!", "IMAGINARY requires 1 argument"}, + "=IMAGINARY(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMARGUMENT - "=IMARGUMENT()": "IMARGUMENT requires 1 argument", - "=IMARGUMENT(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMARGUMENT()": {"#VALUE!", "IMARGUMENT requires 1 argument"}, + "=IMARGUMENT(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMCONJUGATE - "=IMCONJUGATE()": "IMCONJUGATE requires 1 argument", - "=IMCONJUGATE(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMCONJUGATE()": {"#VALUE!", "IMCONJUGATE requires 1 argument"}, + "=IMCONJUGATE(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMCOS - "=IMCOS()": "IMCOS requires 1 argument", - "=IMCOS(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMCOS()": {"#VALUE!", "IMCOS requires 1 argument"}, + "=IMCOS(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMCOSH - "=IMCOSH()": "IMCOSH requires 1 argument", - "=IMCOSH(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMCOSH()": {"#VALUE!", "IMCOSH requires 1 argument"}, + "=IMCOSH(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMCOT - "=IMCOT()": "IMCOT requires 1 argument", - "=IMCOT(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMCOT()": {"#VALUE!", "IMCOT requires 1 argument"}, + "=IMCOT(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMCSC - "=IMCSC()": "IMCSC requires 1 argument", - "=IMCSC(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", - "=IMCSC(0)": "#NUM!", + "=IMCSC()": {"#VALUE!", "IMCSC requires 1 argument"}, + "=IMCSC(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, + "=IMCSC(0)": {"#NUM!", "#NUM!"}, // IMCSCH - "=IMCSCH()": "IMCSCH requires 1 argument", - "=IMCSCH(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", - "=IMCSCH(0)": "#NUM!", + "=IMCSCH()": {"#VALUE!", "IMCSCH requires 1 argument"}, + "=IMCSCH(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, + "=IMCSCH(0)": {"#NUM!", "#NUM!"}, // IMDIV - "=IMDIV()": "IMDIV requires 2 arguments", - "=IMDIV(0,\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", - "=IMDIV(\"\",0)": "strconv.ParseComplex: parsing \"\": invalid syntax", - "=IMDIV(1,0)": "#NUM!", + "=IMDIV()": {"#VALUE!", "IMDIV requires 2 arguments"}, + "=IMDIV(0,\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, + "=IMDIV(\"\",0)": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, + "=IMDIV(1,0)": {"#NUM!", "#NUM!"}, // IMEXP - "=IMEXP()": "IMEXP requires 1 argument", - "=IMEXP(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMEXP()": {"#VALUE!", "IMEXP requires 1 argument"}, + "=IMEXP(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMLN - "=IMLN()": "IMLN requires 1 argument", - "=IMLN(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", - "=IMLN(0)": "#NUM!", + "=IMLN()": {"#VALUE!", "IMLN requires 1 argument"}, + "=IMLN(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, + "=IMLN(0)": {"#NUM!", "#NUM!"}, // IMLOG10 - "=IMLOG10()": "IMLOG10 requires 1 argument", - "=IMLOG10(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", - "=IMLOG10(0)": "#NUM!", + "=IMLOG10()": {"#VALUE!", "IMLOG10 requires 1 argument"}, + "=IMLOG10(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, + "=IMLOG10(0)": {"#NUM!", "#NUM!"}, // IMLOG2 - "=IMLOG2()": "IMLOG2 requires 1 argument", - "=IMLOG2(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", - "=IMLOG2(0)": "#NUM!", + "=IMLOG2()": {"#VALUE!", "IMLOG2 requires 1 argument"}, + "=IMLOG2(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, + "=IMLOG2(0)": {"#NUM!", "#NUM!"}, // IMPOWER - "=IMPOWER()": "IMPOWER requires 2 arguments", - "=IMPOWER(0,\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", - "=IMPOWER(\"\",0)": "strconv.ParseComplex: parsing \"\": invalid syntax", - "=IMPOWER(0,0)": "#NUM!", - "=IMPOWER(0,-1)": "#NUM!", + "=IMPOWER()": {"#VALUE!", "IMPOWER requires 2 arguments"}, + "=IMPOWER(0,\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, + "=IMPOWER(\"\",0)": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, + "=IMPOWER(0,0)": {"#NUM!", "#NUM!"}, + "=IMPOWER(0,-1)": {"#NUM!", "#NUM!"}, // IMPRODUCT - "=IMPRODUCT(\"x\")": "strconv.ParseComplex: parsing \"x\": invalid syntax", - "=IMPRODUCT(A1:D1)": "strconv.ParseComplex: parsing \"Month\": invalid syntax", + "=IMPRODUCT(\"x\")": {"#NUM!", "strconv.ParseComplex: parsing \"x\": invalid syntax"}, + "=IMPRODUCT(A1:D1)": {"#NUM!", "strconv.ParseComplex: parsing \"Month\": invalid syntax"}, // IMREAL - "=IMREAL()": "IMREAL requires 1 argument", - "=IMREAL(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMREAL()": {"#VALUE!", "IMREAL requires 1 argument"}, + "=IMREAL(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMSEC - "=IMSEC()": "IMSEC requires 1 argument", - "=IMSEC(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMSEC()": {"#VALUE!", "IMSEC requires 1 argument"}, + "=IMSEC(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMSECH - "=IMSECH()": "IMSECH requires 1 argument", - "=IMSECH(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMSECH()": {"#VALUE!", "IMSECH requires 1 argument"}, + "=IMSECH(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMSIN - "=IMSIN()": "IMSIN requires 1 argument", - "=IMSIN(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMSIN()": {"#VALUE!", "IMSIN requires 1 argument"}, + "=IMSIN(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMSINH - "=IMSINH()": "IMSINH requires 1 argument", - "=IMSINH(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMSINH()": {"#VALUE!", "IMSINH requires 1 argument"}, + "=IMSINH(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMSQRT - "=IMSQRT()": "IMSQRT requires 1 argument", - "=IMSQRT(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMSQRT()": {"#VALUE!", "IMSQRT requires 1 argument"}, + "=IMSQRT(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMSUB - "=IMSUB()": "IMSUB requires 2 arguments", - "=IMSUB(0,\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", - "=IMSUB(\"\",0)": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMSUB()": {"#VALUE!", "IMSUB requires 2 arguments"}, + "=IMSUB(0,\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, + "=IMSUB(\"\",0)": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMSUM - "=IMSUM()": "IMSUM requires at least 1 argument", - "=IMSUM(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMSUM()": {"#VALUE!", "IMSUM requires at least 1 argument"}, + "=IMSUM(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // IMTAN - "=IMTAN()": "IMTAN requires 1 argument", - "=IMTAN(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMTAN()": {"#VALUE!", "IMTAN requires 1 argument"}, + "=IMTAN(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"}, // OCT2BIN - "=OCT2BIN()": "OCT2BIN requires at least 1 argument", - "=OCT2BIN(1,1,1)": "OCT2BIN allows at most 2 arguments", - "=OCT2BIN(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=OCT2BIN(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=OCT2BIN(-536870912 ,10)": "#NUM!", - "=OCT2BIN(1,-1)": "#NUM!", + "=OCT2BIN()": {"#VALUE!", "OCT2BIN requires at least 1 argument"}, + "=OCT2BIN(1,1,1)": {"#VALUE!", "OCT2BIN allows at most 2 arguments"}, + "=OCT2BIN(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=OCT2BIN(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=OCT2BIN(-536870912 ,10)": {"#NUM!", "#NUM!"}, + "=OCT2BIN(1,-1)": {"#NUM!", "#NUM!"}, // OCT2DEC - "=OCT2DEC()": "OCT2DEC requires 1 numeric argument", - "=OCT2DEC(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=OCT2DEC()": {"#VALUE!", "OCT2DEC requires 1 numeric argument"}, + "=OCT2DEC(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // OCT2HEX - "=OCT2HEX()": "OCT2HEX requires at least 1 argument", - "=OCT2HEX(1,1,1)": "OCT2HEX allows at most 2 arguments", - "=OCT2HEX(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=OCT2HEX(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=OCT2HEX(-536870912 ,10)": "#NUM!", - "=OCT2HEX(1,-1)": "#NUM!", + "=OCT2HEX()": {"#VALUE!", "OCT2HEX requires at least 1 argument"}, + "=OCT2HEX(1,1,1)": {"#VALUE!", "OCT2HEX allows at most 2 arguments"}, + "=OCT2HEX(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=OCT2HEX(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=OCT2HEX(-536870912 ,10)": {"#NUM!", "#NUM!"}, + "=OCT2HEX(1,-1)": {"#NUM!", "#NUM!"}, // Math and Trigonometric Functions // ABS - "=ABS()": "ABS requires 1 numeric argument", - `=ABS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=ABS(~)": newInvalidColumnNameError("~").Error(), + "=ABS()": {"#VALUE!", "ABS requires 1 numeric argument"}, + "=ABS(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=ABS(~)": {"", newInvalidColumnNameError("~").Error()}, // ACOS - "=ACOS()": "ACOS requires 1 numeric argument", - `=ACOS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=ACOS(ACOS(0))": "#NUM!", + "=ACOS()": {"#VALUE!", "ACOS requires 1 numeric argument"}, + "=ACOS(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=ACOS(ACOS(0))": {"#NUM!", "#NUM!"}, // ACOSH - "=ACOSH()": "ACOSH requires 1 numeric argument", - `=ACOSH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ACOSH()": {"#VALUE!", "ACOSH requires 1 numeric argument"}, + "=ACOSH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // _xlfn.ACOT - "=_xlfn.ACOT()": "ACOT requires 1 numeric argument", - `=_xlfn.ACOT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=_xlfn.ACOT()": {"#VALUE!", "ACOT requires 1 numeric argument"}, + "=_xlfn.ACOT(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // _xlfn.ACOTH - "=_xlfn.ACOTH()": "ACOTH requires 1 numeric argument", - `=_xlfn.ACOTH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=_xlfn.ACOTH(_xlfn.ACOTH(2))": "#NUM!", + "=_xlfn.ACOTH()": {"#VALUE!", "ACOTH requires 1 numeric argument"}, + "=_xlfn.ACOTH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=_xlfn.ACOTH(_xlfn.ACOTH(2))": {"#NUM!", "#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.AGGREGATE()": {"#VALUE!", "AGGREGATE requires at least 3 arguments"}, + "=_xlfn.AGGREGATE(\"\",0,A4:A5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=_xlfn.AGGREGATE(1,\"\",A4:A5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=_xlfn.AGGREGATE(0,A4:A5)": {"#VALUE!", "AGGREGATE has invalid function_num"}, + "=_xlfn.AGGREGATE(1,8,A4:A5)": {"#VALUE!", "AGGREGATE has invalid options"}, + "=_xlfn.AGGREGATE(1,0,A5:A6)": {"#DIV/0!", "#DIV/0!"}, + "=_xlfn.AGGREGATE(13,0,A1:A6)": {"#N/A", "#N/A"}, + "=_xlfn.AGGREGATE(18,0,A1:A6,1)": {"#NUM!", "#NUM!"}, // _xlfn.ARABIC - "=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument", - "=_xlfn.ARABIC(\"" + strings.Repeat("I", 256) + "\")": "#VALUE!", + "=_xlfn.ARABIC()": {"#VALUE!", "ARABIC requires 1 numeric argument"}, + "=_xlfn.ARABIC(\"" + strings.Repeat("I", 256) + "\")": {"#VALUE!", "#VALUE!"}, // ASIN - "=ASIN()": "ASIN requires 1 numeric argument", - `=ASIN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ASIN()": {"#VALUE!", "ASIN requires 1 numeric argument"}, + `=ASIN("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // ASINH - "=ASINH()": "ASINH requires 1 numeric argument", - `=ASINH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ASINH()": {"#VALUE!", "ASINH requires 1 numeric argument"}, + `=ASINH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // ATAN - "=ATAN()": "ATAN requires 1 numeric argument", - `=ATAN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ATAN()": {"#VALUE!", "ATAN requires 1 numeric argument"}, + `=ATAN("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // ATANH - "=ATANH()": "ATANH requires 1 numeric argument", - `=ATANH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ATANH()": {"#VALUE!", "ATANH requires 1 numeric argument"}, + `=ATANH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // ATAN2 - "=ATAN2()": "ATAN2 requires 2 numeric arguments", - `=ATAN2("X",0)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=ATAN2(0,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ATAN2()": {"#VALUE!", "ATAN2 requires 2 numeric arguments"}, + `=ATAN2("X",0)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=ATAN2(0,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // BASE - "=BASE()": "BASE requires at least 2 arguments", - "=BASE(1,2,3,4)": "BASE allows at most 3 arguments", - "=BASE(1,1)": "radix must be an integer >= 2 and <= 36", - `=BASE("X",2)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=BASE(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=BASE(1,2,"X")`: "strconv.Atoi: parsing \"X\": invalid syntax", + "=BASE()": {"#VALUE!", "BASE requires at least 2 arguments"}, + "=BASE(1,2,3,4)": {"#VALUE!", "BASE allows at most 3 arguments"}, + "=BASE(1,1)": {"#VALUE!", "radix must be an integer >= 2 and <= 36"}, + `=BASE("X",2)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=BASE(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=BASE(1,2,"X")`: {"#VALUE!", "strconv.Atoi: parsing \"X\": invalid syntax"}, // CEILING - "=CEILING()": "CEILING requires at least 1 argument", - "=CEILING(1,2,3)": "CEILING allows at most 2 arguments", - "=CEILING(1,-1)": "negative sig to CEILING invalid", - `=CEILING("X",0)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=CEILING(0,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=CEILING()": {"#VALUE!", "CEILING requires at least 1 argument"}, + "=CEILING(1,2,3)": {"#VALUE!", "CEILING allows at most 2 arguments"}, + "=CEILING(1,-1)": {"#VALUE!", "negative sig to CEILING invalid"}, + `=CEILING("X",0)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=CEILING(0,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // _xlfn.CEILING.MATH - "=_xlfn.CEILING.MATH()": "CEILING.MATH requires at least 1 argument", - "=_xlfn.CEILING.MATH(1,2,3,4)": "CEILING.MATH allows at most 3 arguments", - `=_xlfn.CEILING.MATH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=_xlfn.CEILING.MATH(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=_xlfn.CEILING.MATH(1,2,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=_xlfn.CEILING.MATH()": {"#VALUE!", "CEILING.MATH requires at least 1 argument"}, + "=_xlfn.CEILING.MATH(1,2,3,4)": {"#VALUE!", "CEILING.MATH allows at most 3 arguments"}, + `=_xlfn.CEILING.MATH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=_xlfn.CEILING.MATH(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=_xlfn.CEILING.MATH(1,2,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // _xlfn.CEILING.PRECISE - "=_xlfn.CEILING.PRECISE()": "CEILING.PRECISE requires at least 1 argument", - "=_xlfn.CEILING.PRECISE(1,2,3)": "CEILING.PRECISE allows at most 2 arguments", - `=_xlfn.CEILING.PRECISE("X",2)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=_xlfn.CEILING.PRECISE(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=_xlfn.CEILING.PRECISE()": {"#VALUE!", "CEILING.PRECISE requires at least 1 argument"}, + "=_xlfn.CEILING.PRECISE(1,2,3)": {"#VALUE!", "CEILING.PRECISE allows at most 2 arguments"}, + `=_xlfn.CEILING.PRECISE("X",2)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=_xlfn.CEILING.PRECISE(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // COMBIN - "=COMBIN()": "COMBIN requires 2 argument", - "=COMBIN(-1,1)": "COMBIN requires number >= number_chosen", - `=COMBIN("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=COMBIN(-1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=COMBIN()": {"#VALUE!", "COMBIN requires 2 argument"}, + "=COMBIN(-1,1)": {"#VALUE!", "COMBIN requires number >= number_chosen"}, + `=COMBIN("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=COMBIN(-1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // _xlfn.COMBINA - "=_xlfn.COMBINA()": "COMBINA requires 2 argument", - "=_xlfn.COMBINA(-1,1)": "COMBINA requires number > number_chosen", - "=_xlfn.COMBINA(-1,-1)": "COMBIN requires number >= number_chosen", - `=_xlfn.COMBINA("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=_xlfn.COMBINA(-1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=_xlfn.COMBINA()": {"#VALUE!", "COMBINA requires 2 argument"}, + "=_xlfn.COMBINA(-1,1)": {"#VALUE!", "COMBINA requires number > number_chosen"}, + "=_xlfn.COMBINA(-1,-1)": {"#VALUE!", "COMBIN requires number >= number_chosen"}, + `=_xlfn.COMBINA("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=_xlfn.COMBINA(-1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // COS - "=COS()": "COS requires 1 numeric argument", - `=COS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=COS()": {"#VALUE!", "COS requires 1 numeric argument"}, + `=COS("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // COSH - "=COSH()": "COSH requires 1 numeric argument", - `=COSH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=COSH()": {"#VALUE!", "COSH requires 1 numeric argument"}, + `=COSH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // _xlfn.COT - "=COT()": "COT requires 1 numeric argument", - `=COT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=COT(0)": "#DIV/0!", + "=COT()": {"#VALUE!", "COT requires 1 numeric argument"}, + `=COT("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=COT(0)": {"#DIV/0!", "#DIV/0!"}, // _xlfn.COTH - "=COTH()": "COTH requires 1 numeric argument", - `=COTH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=COTH(0)": "#DIV/0!", + "=COTH()": {"#VALUE!", "COTH requires 1 numeric argument"}, + `=COTH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=COTH(0)": {"#DIV/0!", "#DIV/0!"}, // _xlfn.CSC - "=_xlfn.CSC()": "CSC requires 1 numeric argument", - `=_xlfn.CSC("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=_xlfn.CSC(0)": "#DIV/0!", + "=_xlfn.CSC()": {"#VALUE!", "CSC requires 1 numeric argument"}, + `=_xlfn.CSC("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=_xlfn.CSC(0)": {"#DIV/0!", "#DIV/0!"}, // _xlfn.CSCH - "=_xlfn.CSCH()": "CSCH requires 1 numeric argument", - `=_xlfn.CSCH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=_xlfn.CSCH(0)": "#DIV/0!", + "=_xlfn.CSCH()": {"#VALUE!", "CSCH requires 1 numeric argument"}, + `=_xlfn.CSCH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=_xlfn.CSCH(0)": {"#DIV/0!", "#DIV/0!"}, // _xlfn.DECIMAL - "=_xlfn.DECIMAL()": "DECIMAL requires 2 numeric arguments", - `=_xlfn.DECIMAL("X", 2)`: "strconv.ParseInt: parsing \"X\": invalid syntax", - `=_xlfn.DECIMAL(2000, "X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=_xlfn.DECIMAL()": {"#VALUE!", "DECIMAL requires 2 numeric arguments"}, + `=_xlfn.DECIMAL("X",2)`: {"#VALUE!", "strconv.ParseInt: parsing \"X\": invalid syntax"}, + `=_xlfn.DECIMAL(2000,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // DEGREES - "=DEGREES()": "DEGREES requires 1 numeric argument", - `=DEGREES("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=DEGREES(0)": "#DIV/0!", + "=DEGREES()": {"#VALUE!", "DEGREES requires 1 numeric argument"}, + `=DEGREES("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=DEGREES(0)": {"#DIV/0!", "#DIV/0!"}, // EVEN - "=EVEN()": "EVEN requires 1 numeric argument", - `=EVEN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=EVEN()": {"#VALUE!", "EVEN requires 1 numeric argument"}, + `=EVEN("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // EXP - "=EXP()": "EXP requires 1 numeric argument", - `=EXP("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=EXP()": {"#VALUE!", "EXP requires 1 numeric argument"}, + `=EXP("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // FACT - "=FACT()": "FACT requires 1 numeric argument", - `=FACT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=FACT(-1)": "#NUM!", + "=FACT()": {"#VALUE!", "FACT requires 1 numeric argument"}, + `=FACT("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=FACT(-1)": {"#NUM!", "#NUM!"}, // FACTDOUBLE - "=FACTDOUBLE()": "FACTDOUBLE requires 1 numeric argument", - `=FACTDOUBLE("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=FACTDOUBLE(-1)": "#NUM!", + "=FACTDOUBLE()": {"#VALUE!", "FACTDOUBLE requires 1 numeric argument"}, + `=FACTDOUBLE("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=FACTDOUBLE(-1)": {"#NUM!", "#NUM!"}, // FLOOR - "=FLOOR()": "FLOOR requires 2 numeric arguments", - `=FLOOR("X",-1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=FLOOR(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=FLOOR(1,-1)": "invalid arguments to FLOOR", + "=FLOOR()": {"#VALUE!", "FLOOR requires 2 numeric arguments"}, + `=FLOOR("X",-1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=FLOOR(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=FLOOR(1,-1)": {"#NUM!", "invalid arguments to FLOOR"}, // _xlfn.FLOOR.MATH - "=_xlfn.FLOOR.MATH()": "FLOOR.MATH requires at least 1 argument", - "=_xlfn.FLOOR.MATH(1,2,3,4)": "FLOOR.MATH allows at most 3 arguments", - `=_xlfn.FLOOR.MATH("X",2,3)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=_xlfn.FLOOR.MATH(1,"X",3)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=_xlfn.FLOOR.MATH(1,2,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=_xlfn.FLOOR.MATH()": {"#VALUE!", "FLOOR.MATH requires at least 1 argument"}, + "=_xlfn.FLOOR.MATH(1,2,3,4)": {"#VALUE!", "FLOOR.MATH allows at most 3 arguments"}, + `=_xlfn.FLOOR.MATH("X",2,3)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=_xlfn.FLOOR.MATH(1,"X",3)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=_xlfn.FLOOR.MATH(1,2,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // _xlfn.FLOOR.PRECISE - "=_xlfn.FLOOR.PRECISE()": "FLOOR.PRECISE requires at least 1 argument", - "=_xlfn.FLOOR.PRECISE(1,2,3)": "FLOOR.PRECISE allows at most 2 arguments", - `=_xlfn.FLOOR.PRECISE("X",2)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=_xlfn.FLOOR.PRECISE(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=_xlfn.FLOOR.PRECISE()": {"#VALUE!", "FLOOR.PRECISE requires at least 1 argument"}, + "=_xlfn.FLOOR.PRECISE(1,2,3)": {"#VALUE!", "FLOOR.PRECISE allows at most 2 arguments"}, + `=_xlfn.FLOOR.PRECISE("X",2)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=_xlfn.FLOOR.PRECISE(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // GCD - "=GCD()": "GCD requires at least 1 argument", - "=GCD(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GCD(-1)": "GCD only accepts positive arguments", - "=GCD(1,-1)": "GCD only accepts positive arguments", - `=GCD("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=GCD()": {"#VALUE!", "GCD requires at least 1 argument"}, + "=GCD(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GCD(-1)": {"#VALUE!", "GCD only accepts positive arguments"}, + "=GCD(1,-1)": {"#VALUE!", "GCD only accepts positive arguments"}, + `=GCD("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // INT - "=INT()": "INT requires 1 numeric argument", - `=INT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=INT()": {"#VALUE!", "INT requires 1 numeric argument"}, + `=INT("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // ISO.CEILING - "=ISO.CEILING()": "ISO.CEILING requires at least 1 argument", - "=ISO.CEILING(1,2,3)": "ISO.CEILING allows at most 2 arguments", - `=ISO.CEILING("X",2)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=ISO.CEILING(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ISO.CEILING()": {"#VALUE!", "ISO.CEILING requires at least 1 argument"}, + "=ISO.CEILING(1,2,3)": {"#VALUE!", "ISO.CEILING allows at most 2 arguments"}, + `=ISO.CEILING("X",2)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=ISO.CEILING(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // LCM - "=LCM()": "LCM requires at least 1 argument", - "=LCM(-1)": "LCM only accepts positive arguments", - "=LCM(1,-1)": "LCM only accepts positive arguments", - `=LCM("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=LCM()": {"#VALUE!", "LCM requires at least 1 argument"}, + "=LCM(-1)": {"#VALUE!", "LCM only accepts positive arguments"}, + "=LCM(1,-1)": {"#VALUE!", "LCM only accepts positive arguments"}, + `=LCM("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // LN - "=LN()": "LN requires 1 numeric argument", - `=LN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=LN()": {"#VALUE!", "LN requires 1 numeric argument"}, + "=LN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // LOG - "=LOG()": "LOG requires at least 1 argument", - "=LOG(1,2,3)": "LOG allows at most 2 arguments", - `=LOG("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=LOG(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=LOG(0,0)": "#DIV/0!", - "=LOG(1,0)": "#DIV/0!", - "=LOG(1,1)": "#DIV/0!", + "=LOG()": {"#VALUE!", "LOG requires at least 1 argument"}, + "=LOG(1,2,3)": {"#VALUE!", "LOG allows at most 2 arguments"}, + `=LOG("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=LOG(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=LOG(0,0)": {"#NUM!", "#DIV/0!"}, + "=LOG(1,0)": {"#NUM!", "#DIV/0!"}, + "=LOG(1,1)": {"#DIV/0!", "#DIV/0!"}, // LOG10 - "=LOG10()": "LOG10 requires 1 numeric argument", - `=LOG10("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=LOG10()": {"#VALUE!", "LOG10 requires 1 numeric argument"}, + "=LOG10(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // MDETERM - "=MDETERM()": "MDETERM requires 1 argument", + "=MDETERM()": {"#VALUE!", "MDETERM requires 1 argument"}, // MINVERSE - "=MINVERSE()": "MINVERSE requires 1 argument", - "=MINVERSE(B3:C4)": "#VALUE!", - "=MINVERSE(A1:C2)": "#VALUE!", - "=MINVERSE(A4:A4)": "#NUM!", + "=MINVERSE()": {"#VALUE!", "MINVERSE requires 1 argument"}, + "=MINVERSE(B3:C4)": {"#VALUE!", "#VALUE!"}, + "=MINVERSE(A1:C2)": {"#VALUE!", "#VALUE!"}, + "=MINVERSE(A4:A4)": {"#NUM!", "#NUM!"}, // MMULT - "=MMULT()": "MMULT requires 2 argument", - "=MMULT(A1:B2,B3:C4)": "#VALUE!", - "=MMULT(B3:C4,A1:B2)": "#VALUE!", - "=MMULT(A1:A2,B1:B2)": "#VALUE!", + "=MMULT()": {"#VALUE!", "MMULT requires 2 argument"}, + "=MMULT(A1:B2,B3:C4)": {"#VALUE!", "#VALUE!"}, + "=MMULT(B3:C4,A1:B2)": {"#VALUE!", "#VALUE!"}, + "=MMULT(A1:A2,B1:B2)": {"#VALUE!", "#VALUE!"}, // MOD - "=MOD()": "MOD requires 2 numeric arguments", - "=MOD(6,0)": "MOD divide by zero", - `=MOD("X",0)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=MOD(6,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=MOD()": {"#VALUE!", "MOD requires 2 numeric arguments"}, + "=MOD(6,0)": {"#DIV/0!", "MOD divide by zero"}, + `=MOD("X",0)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=MOD(6,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // MROUND - "=MROUND()": "MROUND requires 2 numeric arguments", - "=MROUND(1,0)": "#NUM!", - "=MROUND(1,-1)": "#NUM!", - `=MROUND("X",0)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=MROUND(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=MROUND()": {"#VALUE!", "MROUND requires 2 numeric arguments"}, + "=MROUND(1,0)": {"#NUM!", "#NUM!"}, + "=MROUND(1,-1)": {"#NUM!", "#NUM!"}, + `=MROUND("X",0)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=MROUND(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // MULTINOMIAL - `=MULTINOMIAL("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=MULTINOMIAL("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // _xlfn.MUNIT - "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", - `=_xlfn.MUNIT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=_xlfn.MUNIT(-1)": "", + "=_xlfn.MUNIT()": {"#VALUE!", "MUNIT requires 1 numeric argument"}, + `=_xlfn.MUNIT("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=_xlfn.MUNIT(-1)": {"#VALUE!", ""}, // ODD - "=ODD()": "ODD requires 1 numeric argument", - `=ODD("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ODD()": {"#VALUE!", "ODD requires 1 numeric argument"}, + `=ODD("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // PI - "=PI(1)": "PI accepts no arguments", + "=PI(1)": {"#VALUE!", "PI accepts no arguments"}, // POWER - `=POWER("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=POWER(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=POWER(0,0)": "#NUM!", - "=POWER(0,-1)": "#DIV/0!", - "=POWER(1)": "POWER requires 2 numeric arguments", + `=POWER("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=POWER(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=POWER(0,0)": {"#NUM!", "#NUM!"}, + "=POWER(0,-1)": {"#DIV/0!", "#DIV/0!"}, + "=POWER(1)": {"#VALUE!", "POWER requires 2 numeric arguments"}, // PRODUCT - "=PRODUCT(\"X\")": "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=PRODUCT(\"\",3,6)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=PRODUCT(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=PRODUCT(\"\",3,6)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // QUOTIENT - `=QUOTIENT("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=QUOTIENT(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=QUOTIENT(1,0)": "#DIV/0!", - "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", + `=QUOTIENT("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=QUOTIENT(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=QUOTIENT(1,0)": {"#DIV/0!", "#DIV/0!"}, + "=QUOTIENT(1)": {"#VALUE!", "QUOTIENT requires 2 numeric arguments"}, // RADIANS - `=RADIANS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=RADIANS()": "RADIANS requires 1 numeric argument", + `=RADIANS("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=RADIANS()": {"#VALUE!", "RADIANS requires 1 numeric argument"}, // RAND - "=RAND(1)": "RAND accepts no arguments", + "=RAND(1)": {"#VALUE!", "RAND accepts no arguments"}, // RANDBETWEEN - `=RANDBETWEEN("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=RANDBETWEEN(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=RANDBETWEEN()": "RANDBETWEEN requires 2 numeric arguments", - "=RANDBETWEEN(2,1)": "#NUM!", + `=RANDBETWEEN("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=RANDBETWEEN(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=RANDBETWEEN()": {"#VALUE!", "RANDBETWEEN requires 2 numeric arguments"}, + "=RANDBETWEEN(2,1)": {"#NUM!", "#NUM!"}, // ROMAN - "=ROMAN()": "ROMAN requires at least 1 argument", - "=ROMAN(1,2,3)": "ROMAN allows at most 2 arguments", - "=ROMAN(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=ROMAN(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=ROMAN(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=ROMAN()": {"#VALUE!", "ROMAN requires at least 1 argument"}, + "=ROMAN(1,2,3)": {"#VALUE!", "ROMAN allows at most 2 arguments"}, + "=ROMAN(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ROMAN(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ROMAN(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // ROUND - "=ROUND()": "ROUND requires 2 numeric arguments", - `=ROUND("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=ROUND(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ROUND()": {"#VALUE!", "ROUND requires 2 numeric arguments"}, + `=ROUND("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=ROUND(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // ROUNDDOWN - "=ROUNDDOWN()": "ROUNDDOWN requires 2 numeric arguments", - `=ROUNDDOWN("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=ROUNDDOWN(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ROUNDDOWN()": {"#VALUE!", "ROUNDDOWN requires 2 numeric arguments"}, + `=ROUNDDOWN("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=ROUNDDOWN(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // ROUNDUP - "=ROUNDUP()": "ROUNDUP requires 2 numeric arguments", - `=ROUNDUP("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=ROUNDUP(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ROUNDUP()": {"#VALUE!", "ROUNDUP requires 2 numeric arguments"}, + `=ROUNDUP("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + `=ROUNDUP(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // SEC - "=_xlfn.SEC()": "SEC requires 1 numeric argument", - `=_xlfn.SEC("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=_xlfn.SEC()": {"#VALUE!", "SEC requires 1 numeric argument"}, + `=_xlfn.SEC("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // _xlfn.SECH - "=_xlfn.SECH()": "SECH requires 1 numeric argument", - `=_xlfn.SECH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=_xlfn.SECH()": {"#VALUE!", "SECH requires 1 numeric argument"}, + `=_xlfn.SECH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // SERIESSUM - "=SERIESSUM()": "SERIESSUM requires 4 arguments", - "=SERIESSUM(\"\",2,3,A1:A4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=SERIESSUM(1,\"\",3,A1:A4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=SERIESSUM(1,2,\"\",A1:A4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=SERIESSUM(1,2,3,A1:D1)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", + "=SERIESSUM()": {"#VALUE!", "SERIESSUM requires 4 arguments"}, + "=SERIESSUM(\"\",2,3,A1:A4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=SERIESSUM(1,\"\",3,A1:A4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=SERIESSUM(1,2,\"\",A1:A4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=SERIESSUM(1,2,3,A1:D1)": {"#VALUE!", "strconv.ParseFloat: parsing \"Month\": invalid syntax"}, // SIGN - "=SIGN()": "SIGN requires 1 numeric argument", - `=SIGN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=SIGN()": {"#VALUE!", "SIGN requires 1 numeric argument"}, + `=SIGN("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // SIN - "=SIN()": "SIN requires 1 numeric argument", - `=SIN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=SIN()": {"#VALUE!", "SIN requires 1 numeric argument"}, + `=SIN("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // SINH - "=SINH()": "SINH requires 1 numeric argument", - `=SINH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=SINH()": {"#VALUE!", "SINH requires 1 numeric argument"}, + `=SINH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // SQRT - "=SQRT()": "SQRT requires 1 numeric argument", - `=SQRT("")`: "strconv.ParseFloat: parsing \"\": invalid syntax", - `=SQRT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=SQRT(-1)": "#NUM!", + "=SQRT()": {"#VALUE!", "SQRT requires 1 numeric argument"}, + `=SQRT("")`: {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + `=SQRT("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=SQRT(-1)": {"#NUM!", "#NUM!"}, // SQRTPI - "=SQRTPI()": "SQRTPI requires 1 numeric argument", - `=SQRTPI("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=SQRTPI()": {"#VALUE!", "SQRTPI requires 1 numeric argument"}, + `=SQRTPI("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // STDEV - "=STDEV()": "STDEV requires at least 1 argument", - "=STDEV(E2:E9)": "#DIV/0!", + "=STDEV()": {"#VALUE!", "STDEV requires at least 1 argument"}, + "=STDEV(E2:E9)": {"#DIV/0!", "#DIV/0!"}, // STDEV.S - "=STDEV.S()": "STDEV.S requires at least 1 argument", + "=STDEV.S()": {"#VALUE!", "STDEV.S requires at least 1 argument"}, // STDEVA - "=STDEVA()": "STDEVA requires at least 1 argument", - "=STDEVA(E2:E9)": "#DIV/0!", + "=STDEVA()": {"#VALUE!", "STDEVA requires at least 1 argument"}, + "=STDEVA(E2:E9)": {"#DIV/0!", "#DIV/0!"}, // POISSON.DIST - "=POISSON.DIST()": "POISSON.DIST requires 3 arguments", + "=POISSON.DIST()": {"#VALUE!", "POISSON.DIST requires 3 arguments"}, // POISSON - "=POISSON()": "POISSON requires 3 arguments", - "=POISSON(\"\",0,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=POISSON(0,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=POISSON(0,0,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=POISSON(0,-1,TRUE)": "#N/A", + "=POISSON()": {"#VALUE!", "POISSON requires 3 arguments"}, + "=POISSON(\"\",0,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=POISSON(0,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=POISSON(0,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=POISSON(0,-1,TRUE)": {"#N/A", "#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!", + "=SUBTOTAL()": {"#VALUE!", "SUBTOTAL requires at least 2 arguments"}, + "=SUBTOTAL(\"\",A4:A5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=SUBTOTAL(0,A4:A5)": {"#VALUE!", "SUBTOTAL has invalid function_num"}, + "=SUBTOTAL(1,A5:A6)": {"#DIV/0!", "#DIV/0!"}, // SUM - "=SUM((": ErrInvalidFormula.Error(), - "=SUM(-)": ErrInvalidFormula.Error(), - "=SUM(1+)": ErrInvalidFormula.Error(), - "=SUM(1-)": ErrInvalidFormula.Error(), - "=SUM(1*)": ErrInvalidFormula.Error(), - "=SUM(1/)": ErrInvalidFormula.Error(), - "=SUM(1*SUM(1/0))": "#DIV/0!", - "=SUM(1*SUM(1/0)*1)": "#DIV/0!", + "=SUM((": {"", ErrInvalidFormula.Error()}, + "=SUM(-)": {ErrInvalidFormula.Error(), ErrInvalidFormula.Error()}, + "=SUM(1+)": {ErrInvalidFormula.Error(), ErrInvalidFormula.Error()}, + "=SUM(1-)": {ErrInvalidFormula.Error(), ErrInvalidFormula.Error()}, + "=SUM(1*)": {ErrInvalidFormula.Error(), ErrInvalidFormula.Error()}, + "=SUM(1/)": {ErrInvalidFormula.Error(), ErrInvalidFormula.Error()}, + "=SUM(1*SUM(1/0))": {"#DIV/0!", "#DIV/0!"}, + "=SUM(1*SUM(1/0)*1)": {"", "#DIV/0!"}, // SUMIF - "=SUMIF()": "SUMIF requires at least 2 arguments", + "=SUMIF()": {"#VALUE!", "SUMIF requires at least 2 arguments"}, // SUMSQ - `=SUMSQ("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=SUMSQ(C1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", + "=SUMSQ(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=SUMSQ(C1:D2)": {"#VALUE!", "strconv.ParseFloat: parsing \"Month\": invalid syntax"}, // SUMPRODUCT - "=SUMPRODUCT()": "SUMPRODUCT requires at least 1 argument", - "=SUMPRODUCT(A1,B1:B2)": "#VALUE!", - "=SUMPRODUCT(A1,D1)": "#VALUE!", - "=SUMPRODUCT(A1:A3,D1:D3)": "#VALUE!", - "=SUMPRODUCT(A1:A2,B1:B3)": "#VALUE!", - "=SUMPRODUCT(\"\")": "#VALUE!", - "=SUMPRODUCT(A1,NA())": "#N/A", + "=SUMPRODUCT()": {"#VALUE!", "SUMPRODUCT requires at least 1 argument"}, + "=SUMPRODUCT(A1,B1:B2)": {"#VALUE!", "#VALUE!"}, + "=SUMPRODUCT(A1,D1)": {"#VALUE!", "#VALUE!"}, + "=SUMPRODUCT(A1:A3,D1:D3)": {"#VALUE!", "#VALUE!"}, + "=SUMPRODUCT(A1:A2,B1:B3)": {"#VALUE!", "#VALUE!"}, + "=SUMPRODUCT(\"\")": {"#VALUE!", "#VALUE!"}, + "=SUMPRODUCT(A1,NA())": {"#N/A", "#N/A"}, // SUMX2MY2 - "=SUMX2MY2()": "SUMX2MY2 requires 2 arguments", - "=SUMX2MY2(A1,B1:B2)": "#N/A", + "=SUMX2MY2()": {"#VALUE!", "SUMX2MY2 requires 2 arguments"}, + "=SUMX2MY2(A1,B1:B2)": {"#N/A", "#N/A"}, // SUMX2PY2 - "=SUMX2PY2()": "SUMX2PY2 requires 2 arguments", - "=SUMX2PY2(A1,B1:B2)": "#N/A", + "=SUMX2PY2()": {"#VALUE!", "SUMX2PY2 requires 2 arguments"}, + "=SUMX2PY2(A1,B1:B2)": {"#N/A", "#N/A"}, // SUMXMY2 - "=SUMXMY2()": "SUMXMY2 requires 2 arguments", - "=SUMXMY2(A1,B1:B2)": "#N/A", + "=SUMXMY2()": {"#VALUE!", "SUMXMY2 requires 2 arguments"}, + "=SUMXMY2(A1,B1:B2)": {"#N/A", "#N/A"}, // TAN - "=TAN()": "TAN requires 1 numeric argument", - `=TAN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=TAN()": {"#VALUE!", "TAN requires 1 numeric argument"}, + "=TAN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // TANH - "=TANH()": "TANH requires 1 numeric argument", - `=TANH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=TANH()": {"#VALUE!", "TANH requires 1 numeric argument"}, + "=TANH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // TRUNC - "=TRUNC()": "TRUNC requires at least 1 argument", - `=TRUNC("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - `=TRUNC(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=TRUNC()": {"#VALUE!", "TRUNC requires at least 1 argument"}, + "=TRUNC(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=TRUNC(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // Statistical Functions // AVEDEV - "=AVEDEV()": "AVEDEV requires at least 1 argument", - "=AVEDEV(\"\")": "#VALUE!", - "=AVEDEV(1,\"\")": "#VALUE!", + "=AVEDEV()": {"#VALUE!", "AVEDEV requires at least 1 argument"}, + "=AVEDEV(\"\")": {"#VALUE!", "#VALUE!"}, + "=AVEDEV(1,\"\")": {"#VALUE!", "#VALUE!"}, // AVERAGE - "=AVERAGE(H1)": "#DIV/0!", + "=AVERAGE(H1)": {"#DIV/0!", "#DIV/0!"}, // AVERAGEA - "=AVERAGEA(H1)": "#DIV/0!", + "=AVERAGEA(H1)": {"#DIV/0!", "#DIV/0!"}, // AVERAGEIF - "=AVERAGEIF()": "AVERAGEIF requires at least 2 arguments", - "=AVERAGEIF(H1,\"\")": "#DIV/0!", - "=AVERAGEIF(D1:D3,\"Month\",D1:D3)": "#DIV/0!", - "=AVERAGEIF(C1:C3,\"Month\",D1:D3)": "#DIV/0!", + "=AVERAGEIF()": {"#VALUE!", "AVERAGEIF requires at least 2 arguments"}, + "=AVERAGEIF(H1,\"\")": {"#DIV/0!", "#DIV/0!"}, + "=AVERAGEIF(D1:D3,\"Month\",D1:D3)": {"#DIV/0!", "#DIV/0!"}, + "=AVERAGEIF(C1:C3,\"Month\",D1:D3)": {"#DIV/0!", "#DIV/0!"}, // BETA.DIST - "=BETA.DIST()": "BETA.DIST requires at least 4 arguments", - "=BETA.DIST(0.4,4,5,TRUE,0,1,0)": "BETA.DIST requires at most 6 arguments", - "=BETA.DIST(\"\",4,5,TRUE,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETA.DIST(0.4,\"\",5,TRUE,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETA.DIST(0.4,4,\"\",TRUE,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETA.DIST(0.4,4,5,\"\",0,1)": "strconv.ParseBool: parsing \"\": invalid syntax", - "=BETA.DIST(0.4,4,5,TRUE,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETA.DIST(0.4,4,5,TRUE,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETA.DIST(0.4,0,5,TRUE,0,1)": "#NUM!", - "=BETA.DIST(0.4,4,0,TRUE,0,0)": "#NUM!", - "=BETA.DIST(0.4,4,5,TRUE,0.5,1)": "#NUM!", - "=BETA.DIST(0.4,4,5,TRUE,0,0.3)": "#NUM!", - "=BETA.DIST(0.4,4,5,TRUE,0.4,0.4)": "#NUM!", + "=BETA.DIST()": {"#VALUE!", "BETA.DIST requires at least 4 arguments"}, + "=BETA.DIST(0.4,4,5,TRUE,0,1,0)": {"#VALUE!", "BETA.DIST requires at most 6 arguments"}, + "=BETA.DIST(\"\",4,5,TRUE,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETA.DIST(0.4,\"\",5,TRUE,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETA.DIST(0.4,4,\"\",TRUE,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETA.DIST(0.4,4,5,\"\",0,1)": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=BETA.DIST(0.4,4,5,TRUE,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETA.DIST(0.4,4,5,TRUE,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETA.DIST(0.4,0,5,TRUE,0,1)": {"#NUM!", "#NUM!"}, + "=BETA.DIST(0.4,4,0,TRUE,0,0)": {"#NUM!", "#NUM!"}, + "=BETA.DIST(0.4,4,5,TRUE,0.5,1)": {"#NUM!", "#NUM!"}, + "=BETA.DIST(0.4,4,5,TRUE,0,0.3)": {"#NUM!", "#NUM!"}, + "=BETA.DIST(0.4,4,5,TRUE,0.4,0.4)": {"#NUM!", "#NUM!"}, // BETADIST - "=BETADIST()": "BETADIST requires at least 3 arguments", - "=BETADIST(0.4,4,5,0,1,0)": "BETADIST requires at most 5 arguments", - "=BETADIST(\"\",4,5,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETADIST(0.4,\"\",5,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETADIST(0.4,4,\"\",0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETADIST(0.4,4,5,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETADIST(0.4,4,5,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETADIST(2,4,5,3,1)": "#NUM!", - "=BETADIST(2,4,5,0,1)": "#NUM!", - "=BETADIST(0.4,0,5,0,1)": "#NUM!", - "=BETADIST(0.4,4,0,0,1)": "#NUM!", - "=BETADIST(0.4,4,5,0.4,0.4)": "#NUM!", + "=BETADIST()": {"#VALUE!", "BETADIST requires at least 3 arguments"}, + "=BETADIST(0.4,4,5,0,1,0)": {"#VALUE!", "BETADIST requires at most 5 arguments"}, + "=BETADIST(\"\",4,5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETADIST(0.4,\"\",5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETADIST(0.4,4,\"\",0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETADIST(0.4,4,5,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETADIST(0.4,4,5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETADIST(2,4,5,3,1)": {"#NUM!", "#NUM!"}, + "=BETADIST(2,4,5,0,1)": {"#NUM!", "#NUM!"}, + "=BETADIST(0.4,0,5,0,1)": {"#NUM!", "#NUM!"}, + "=BETADIST(0.4,4,0,0,1)": {"#NUM!", "#NUM!"}, + "=BETADIST(0.4,4,5,0.4,0.4)": {"#NUM!", "#NUM!"}, // BETAINV - "=BETAINV()": "BETAINV requires at least 3 arguments", - "=BETAINV(0.2,4,5,0,1,0)": "BETAINV requires at most 5 arguments", - "=BETAINV(\"\",4,5,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETAINV(0.2,\"\",5,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETAINV(0.2,4,\"\",0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETAINV(0.2,4,5,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETAINV(0.2,4,5,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETAINV(0,4,5,0,1)": "#NUM!", - "=BETAINV(1,4,5,0,1)": "#NUM!", - "=BETAINV(0.2,0,5,0,1)": "#NUM!", - "=BETAINV(0.2,4,0,0,1)": "#NUM!", - "=BETAINV(0.2,4,5,2,2)": "#NUM!", + "=BETAINV()": {"#VALUE!", "BETAINV requires at least 3 arguments"}, + "=BETAINV(0.2,4,5,0,1,0)": {"#VALUE!", "BETAINV requires at most 5 arguments"}, + "=BETAINV(\"\",4,5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETAINV(0.2,\"\",5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETAINV(0.2,4,\"\",0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETAINV(0.2,4,5,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETAINV(0.2,4,5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETAINV(0,4,5,0,1)": {"#NUM!", "#NUM!"}, + "=BETAINV(1,4,5,0,1)": {"#NUM!", "#NUM!"}, + "=BETAINV(0.2,0,5,0,1)": {"#NUM!", "#NUM!"}, + "=BETAINV(0.2,4,0,0,1)": {"#NUM!", "#NUM!"}, + "=BETAINV(0.2,4,5,2,2)": {"#NUM!", "#NUM!"}, // BETA.INV - "=BETA.INV()": "BETA.INV requires at least 3 arguments", - "=BETA.INV(0.2,4,5,0,1,0)": "BETA.INV requires at most 5 arguments", - "=BETA.INV(\"\",4,5,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETA.INV(0.2,\"\",5,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETA.INV(0.2,4,\"\",0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETA.INV(0.2,4,5,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETA.INV(0.2,4,5,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BETA.INV(0,4,5,0,1)": "#NUM!", - "=BETA.INV(1,4,5,0,1)": "#NUM!", - "=BETA.INV(0.2,0,5,0,1)": "#NUM!", - "=BETA.INV(0.2,4,0,0,1)": "#NUM!", - "=BETA.INV(0.2,4,5,2,2)": "#NUM!", + "=BETA.INV()": {"#VALUE!", "BETA.INV requires at least 3 arguments"}, + "=BETA.INV(0.2,4,5,0,1,0)": {"#VALUE!", "BETA.INV requires at most 5 arguments"}, + "=BETA.INV(\"\",4,5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETA.INV(0.2,\"\",5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETA.INV(0.2,4,\"\",0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETA.INV(0.2,4,5,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETA.INV(0.2,4,5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BETA.INV(0,4,5,0,1)": {"#NUM!", "#NUM!"}, + "=BETA.INV(1,4,5,0,1)": {"#NUM!", "#NUM!"}, + "=BETA.INV(0.2,0,5,0,1)": {"#NUM!", "#NUM!"}, + "=BETA.INV(0.2,4,0,0,1)": {"#NUM!", "#NUM!"}, + "=BETA.INV(0.2,4,5,2,2)": {"#NUM!", "#NUM!"}, // BINOMDIST - "=BINOMDIST()": "BINOMDIST requires 4 arguments", - "=BINOMDIST(\"\",100,0.5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOMDIST(10,\"\",0.5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOMDIST(10,100,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOMDIST(10,100,0.5,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=BINOMDIST(-1,100,0.5,FALSE)": "#NUM!", - "=BINOMDIST(110,100,0.5,FALSE)": "#NUM!", - "=BINOMDIST(10,100,-1,FALSE)": "#NUM!", - "=BINOMDIST(10,100,2,FALSE)": "#NUM!", + "=BINOMDIST()": {"#VALUE!", "BINOMDIST requires 4 arguments"}, + "=BINOMDIST(\"\",100,0.5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOMDIST(10,\"\",0.5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOMDIST(10,100,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOMDIST(10,100,0.5,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=BINOMDIST(-1,100,0.5,FALSE)": {"#NUM!", "#NUM!"}, + "=BINOMDIST(110,100,0.5,FALSE)": {"#NUM!", "#NUM!"}, + "=BINOMDIST(10,100,-1,FALSE)": {"#NUM!", "#NUM!"}, + "=BINOMDIST(10,100,2,FALSE)": {"#NUM!", "#NUM!"}, // BINOM.DIST - "=BINOM.DIST()": "BINOM.DIST requires 4 arguments", - "=BINOM.DIST(\"\",100,0.5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOM.DIST(10,\"\",0.5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOM.DIST(10,100,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOM.DIST(10,100,0.5,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=BINOM.DIST(-1,100,0.5,FALSE)": "#NUM!", - "=BINOM.DIST(110,100,0.5,FALSE)": "#NUM!", - "=BINOM.DIST(10,100,-1,FALSE)": "#NUM!", - "=BINOM.DIST(10,100,2,FALSE)": "#NUM!", + "=BINOM.DIST()": {"#VALUE!", "BINOM.DIST requires 4 arguments"}, + "=BINOM.DIST(\"\",100,0.5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOM.DIST(10,\"\",0.5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOM.DIST(10,100,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOM.DIST(10,100,0.5,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=BINOM.DIST(-1,100,0.5,FALSE)": {"#NUM!", "#NUM!"}, + "=BINOM.DIST(110,100,0.5,FALSE)": {"#NUM!", "#NUM!"}, + "=BINOM.DIST(10,100,-1,FALSE)": {"#NUM!", "#NUM!"}, + "=BINOM.DIST(10,100,2,FALSE)": {"#NUM!", "#NUM!"}, // BINOM.DIST.RANGE - "=BINOM.DIST.RANGE()": "BINOM.DIST.RANGE requires at least 3 arguments", - "=BINOM.DIST.RANGE(100,0.5,0,40,0)": "BINOM.DIST.RANGE requires at most 4 arguments", - "=BINOM.DIST.RANGE(\"\",0.5,0,40)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOM.DIST.RANGE(100,\"\",0,40)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOM.DIST.RANGE(100,0.5,\"\",40)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOM.DIST.RANGE(100,0.5,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOM.DIST.RANGE(100,-1,0,40)": "#NUM!", - "=BINOM.DIST.RANGE(100,2,0,40)": "#NUM!", - "=BINOM.DIST.RANGE(100,0.5,-1,40)": "#NUM!", - "=BINOM.DIST.RANGE(100,0.5,110,40)": "#NUM!", - "=BINOM.DIST.RANGE(100,0.5,0,-1)": "#NUM!", - "=BINOM.DIST.RANGE(100,0.5,0,110)": "#NUM!", + "=BINOM.DIST.RANGE()": {"#VALUE!", "BINOM.DIST.RANGE requires at least 3 arguments"}, + "=BINOM.DIST.RANGE(100,0.5,0,40,0)": {"#VALUE!", "BINOM.DIST.RANGE requires at most 4 arguments"}, + "=BINOM.DIST.RANGE(\"\",0.5,0,40)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOM.DIST.RANGE(100,\"\",0,40)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOM.DIST.RANGE(100,0.5,\"\",40)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOM.DIST.RANGE(100,0.5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOM.DIST.RANGE(100,-1,0,40)": {"#NUM!", "#NUM!"}, + "=BINOM.DIST.RANGE(100,2,0,40)": {"#NUM!", "#NUM!"}, + "=BINOM.DIST.RANGE(100,0.5,-1,40)": {"#NUM!", "#NUM!"}, + "=BINOM.DIST.RANGE(100,0.5,110,40)": {"#NUM!", "#NUM!"}, + "=BINOM.DIST.RANGE(100,0.5,0,-1)": {"#NUM!", "#NUM!"}, + "=BINOM.DIST.RANGE(100,0.5,0,110)": {"#NUM!", "#NUM!"}, // BINOM.INV - "=BINOM.INV()": "BINOM.INV requires 3 numeric arguments", - "=BINOM.INV(\"\",0.5,20%)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOM.INV(100,\"\",20%)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOM.INV(100,0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=BINOM.INV(-1,0.5,20%)": "#NUM!", - "=BINOM.INV(100,-1,20%)": "#NUM!", - "=BINOM.INV(100,2,20%)": "#NUM!", - "=BINOM.INV(100,0.5,-1)": "#NUM!", - "=BINOM.INV(100,0.5,2)": "#NUM!", - "=BINOM.INV(1,1,20%)": "#NUM!", + "=BINOM.INV()": {"#VALUE!", "BINOM.INV requires 3 numeric arguments"}, + "=BINOM.INV(\"\",0.5,20%)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOM.INV(100,\"\",20%)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOM.INV(100,0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=BINOM.INV(-1,0.5,20%)": {"#NUM!", "#NUM!"}, + "=BINOM.INV(100,-1,20%)": {"#NUM!", "#NUM!"}, + "=BINOM.INV(100,2,20%)": {"#NUM!", "#NUM!"}, + "=BINOM.INV(100,0.5,-1)": {"#NUM!", "#NUM!"}, + "=BINOM.INV(100,0.5,2)": {"#NUM!", "#NUM!"}, + "=BINOM.INV(1,1,20%)": {"#NUM!", "#NUM!"}, // CHIDIST - "=CHIDIST()": "CHIDIST requires 2 numeric arguments", - "=CHIDIST(\"\",3)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CHIDIST(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHIDIST()": {"#VALUE!", "CHIDIST requires 2 numeric arguments"}, + "=CHIDIST(\"\",3)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CHIDIST(0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // CHIINV - "=CHIINV()": "CHIINV requires 2 numeric arguments", - "=CHIINV(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CHIINV(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CHIINV(0,1)": "#NUM!", - "=CHIINV(2,1)": "#NUM!", - "=CHIINV(0.5,0.5)": "#NUM!", + "=CHIINV()": {"#VALUE!", "CHIINV requires 2 numeric arguments"}, + "=CHIINV(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CHIINV(0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CHIINV(0,1)": {"#NUM!", "#NUM!"}, + "=CHIINV(2,1)": {"#NUM!", "#NUM!"}, + "=CHIINV(0.5,0.5)": {"#NUM!", "#NUM!"}, // CHISQ.DIST - "=CHISQ.DIST()": "CHISQ.DIST requires 3 arguments", - "=CHISQ.DIST(\"\",2,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CHISQ.DIST(3,\"\",TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CHISQ.DIST(3,2,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=CHISQ.DIST(-1,2,TRUE)": "#NUM!", - "=CHISQ.DIST(3,0,TRUE)": "#NUM!", + "=CHISQ.DIST()": {"#VALUE!", "CHISQ.DIST requires 3 arguments"}, + "=CHISQ.DIST(\"\",2,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CHISQ.DIST(3,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CHISQ.DIST(3,2,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=CHISQ.DIST(-1,2,TRUE)": {"#NUM!", "#NUM!"}, + "=CHISQ.DIST(3,0,TRUE)": {"#NUM!", "#NUM!"}, // CHISQ.DIST.RT - "=CHISQ.DIST.RT()": "CHISQ.DIST.RT requires 2 numeric arguments", - "=CHISQ.DIST.RT(\"\",3)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CHISQ.DIST.RT(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHISQ.DIST.RT()": {"#VALUE!", "CHISQ.DIST.RT requires 2 numeric arguments"}, + "=CHISQ.DIST.RT(\"\",3)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CHISQ.DIST.RT(0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // CHISQ.INV - "=CHISQ.INV()": "CHISQ.INV requires 2 numeric arguments", - "=CHISQ.INV(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CHISQ.INV(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CHISQ.INV(-1,1)": "#NUM!", - "=CHISQ.INV(1,1)": "#NUM!", - "=CHISQ.INV(0.5,0.5)": "#NUM!", - "=CHISQ.INV(0.5,10000000001)": "#NUM!", + "=CHISQ.INV()": {"#VALUE!", "CHISQ.INV requires 2 numeric arguments"}, + "=CHISQ.INV(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CHISQ.INV(0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CHISQ.INV(-1,1)": {"#NUM!", "#NUM!"}, + "=CHISQ.INV(1,1)": {"#NUM!", "#NUM!"}, + "=CHISQ.INV(0.5,0.5)": {"#NUM!", "#NUM!"}, + "=CHISQ.INV(0.5,10000000001)": {"#NUM!", "#NUM!"}, // CHISQ.INV.RT - "=CHISQ.INV.RT()": "CHISQ.INV.RT requires 2 numeric arguments", - "=CHISQ.INV.RT(\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CHISQ.INV.RT(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CHISQ.INV.RT(0,1)": "#NUM!", - "=CHISQ.INV.RT(2,1)": "#NUM!", - "=CHISQ.INV.RT(0.5,0.5)": "#NUM!", + "=CHISQ.INV.RT()": {"#VALUE!", "CHISQ.INV.RT requires 2 numeric arguments"}, + "=CHISQ.INV.RT(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CHISQ.INV.RT(0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CHISQ.INV.RT(0,1)": {"#NUM!", "#NUM!"}, + "=CHISQ.INV.RT(2,1)": {"#NUM!", "#NUM!"}, + "=CHISQ.INV.RT(0.5,0.5)": {"#NUM!", "#NUM!"}, // CONFIDENCE - "=CONFIDENCE()": "CONFIDENCE requires 3 numeric arguments", - "=CONFIDENCE(\"\",0.07,100)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CONFIDENCE(0.05,\"\",100)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CONFIDENCE(0.05,0.07,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CONFIDENCE(0,0.07,100)": "#NUM!", - "=CONFIDENCE(1,0.07,100)": "#NUM!", - "=CONFIDENCE(0.05,0,100)": "#NUM!", - "=CONFIDENCE(0.05,0.07,0.5)": "#NUM!", + "=CONFIDENCE()": {"#VALUE!", "CONFIDENCE requires 3 numeric arguments"}, + "=CONFIDENCE(\"\",0.07,100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CONFIDENCE(0.05,\"\",100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CONFIDENCE(0.05,0.07,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CONFIDENCE(0,0.07,100)": {"#NUM!", "#NUM!"}, + "=CONFIDENCE(1,0.07,100)": {"#NUM!", "#NUM!"}, + "=CONFIDENCE(0.05,0,100)": {"#NUM!", "#NUM!"}, + "=CONFIDENCE(0.05,0.07,0.5)": {"#NUM!", "#NUM!"}, // CONFIDENCE.NORM - "=CONFIDENCE.NORM()": "CONFIDENCE.NORM requires 3 numeric arguments", - "=CONFIDENCE.NORM(\"\",0.07,100)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CONFIDENCE.NORM(0.05,\"\",100)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CONFIDENCE.NORM(0.05,0.07,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CONFIDENCE.NORM(0,0.07,100)": "#NUM!", - "=CONFIDENCE.NORM(1,0.07,100)": "#NUM!", - "=CONFIDENCE.NORM(0.05,0,100)": "#NUM!", - "=CONFIDENCE.NORM(0.05,0.07,0.5)": "#NUM!", + "=CONFIDENCE.NORM()": {"#VALUE!", "CONFIDENCE.NORM requires 3 numeric arguments"}, + "=CONFIDENCE.NORM(\"\",0.07,100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CONFIDENCE.NORM(0.05,\"\",100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CONFIDENCE.NORM(0.05,0.07,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CONFIDENCE.NORM(0,0.07,100)": {"#NUM!", "#NUM!"}, + "=CONFIDENCE.NORM(1,0.07,100)": {"#NUM!", "#NUM!"}, + "=CONFIDENCE.NORM(0.05,0,100)": {"#NUM!", "#NUM!"}, + "=CONFIDENCE.NORM(0.05,0.07,0.5)": {"#NUM!", "#NUM!"}, // CORREL - "=CORREL()": "CORREL requires 2 arguments", - "=CORREL(A1:A3,B1:B5)": "#N/A", - "=CORREL(A1:A1,B1:B1)": "#DIV/0!", + "=CORREL()": {"#VALUE!", "CORREL requires 2 arguments"}, + "=CORREL(A1:A3,B1:B5)": {"#N/A", "#N/A"}, + "=CORREL(A1:A1,B1:B1)": {"#DIV/0!", "#DIV/0!"}, // CONFIDENCE.T - "=CONFIDENCE.T()": "CONFIDENCE.T requires 3 arguments", - "=CONFIDENCE.T(\"\",0.07,100)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CONFIDENCE.T(0.05,\"\",100)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CONFIDENCE.T(0.05,0.07,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CONFIDENCE.T(0,0.07,100)": "#NUM!", - "=CONFIDENCE.T(1,0.07,100)": "#NUM!", - "=CONFIDENCE.T(0.05,0,100)": "#NUM!", - "=CONFIDENCE.T(0.05,0.07,0)": "#NUM!", - "=CONFIDENCE.T(0.05,0.07,1)": "#DIV/0!", + "=CONFIDENCE.T()": {"#VALUE!", "CONFIDENCE.T requires 3 arguments"}, + "=CONFIDENCE.T(\"\",0.07,100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CONFIDENCE.T(0.05,\"\",100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CONFIDENCE.T(0.05,0.07,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CONFIDENCE.T(0,0.07,100)": {"#NUM!", "#NUM!"}, + "=CONFIDENCE.T(1,0.07,100)": {"#NUM!", "#NUM!"}, + "=CONFIDENCE.T(0.05,0,100)": {"#NUM!", "#NUM!"}, + "=CONFIDENCE.T(0.05,0.07,0)": {"#NUM!", "#NUM!"}, + "=CONFIDENCE.T(0.05,0.07,1)": {"#DIV/0!", "#DIV/0!"}, // COUNTBLANK - "=COUNTBLANK()": "COUNTBLANK requires 1 argument", - "=COUNTBLANK(1,2)": "COUNTBLANK requires 1 argument", + "=COUNTBLANK()": {"#VALUE!", "COUNTBLANK requires 1 argument"}, + "=COUNTBLANK(1,2)": {"#VALUE!", "COUNTBLANK requires 1 argument"}, // COUNTIF - "=COUNTIF()": "COUNTIF requires 2 arguments", + "=COUNTIF()": {"#VALUE!", "COUNTIF requires 2 arguments"}, // COUNTIFS - "=COUNTIFS()": "COUNTIFS requires at least 2 arguments", - "=COUNTIFS(A1:A9,2,D1:D9)": "#N/A", + "=COUNTIFS()": {"#VALUE!", "COUNTIFS requires at least 2 arguments"}, + "=COUNTIFS(A1:A9,2,D1:D9)": {"#N/A", "#N/A"}, // CRITBINOM - "=CRITBINOM()": "CRITBINOM requires 3 numeric arguments", - "=CRITBINOM(\"\",0.5,20%)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CRITBINOM(100,\"\",20%)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CRITBINOM(100,0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CRITBINOM(-1,0.5,20%)": "#NUM!", - "=CRITBINOM(100,-1,20%)": "#NUM!", - "=CRITBINOM(100,2,20%)": "#NUM!", - "=CRITBINOM(100,0.5,-1)": "#NUM!", - "=CRITBINOM(100,0.5,2)": "#NUM!", - "=CRITBINOM(1,1,20%)": "#NUM!", + "=CRITBINOM()": {"#VALUE!", "CRITBINOM requires 3 numeric arguments"}, + "=CRITBINOM(\"\",0.5,20%)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CRITBINOM(100,\"\",20%)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CRITBINOM(100,0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CRITBINOM(-1,0.5,20%)": {"#NUM!", "#NUM!"}, + "=CRITBINOM(100,-1,20%)": {"#NUM!", "#NUM!"}, + "=CRITBINOM(100,2,20%)": {"#NUM!", "#NUM!"}, + "=CRITBINOM(100,0.5,-1)": {"#NUM!", "#NUM!"}, + "=CRITBINOM(100,0.5,2)": {"#NUM!", "#NUM!"}, + "=CRITBINOM(1,1,20%)": {"#NUM!", "#NUM!"}, // DEVSQ - "=DEVSQ()": "DEVSQ requires at least 1 numeric argument", - "=DEVSQ(D1:D2)": "#N/A", + "=DEVSQ()": {"#VALUE!", "DEVSQ requires at least 1 numeric argument"}, + "=DEVSQ(D1:D2)": {"#N/A", "#N/A"}, // FISHER - "=FISHER()": "FISHER requires 1 numeric argument", - "=FISHER(2)": "#N/A", - "=FISHER(\"2\")": "#N/A", - "=FISHER(INT(-2)))": "#N/A", - "=FISHER(F1)": "FISHER requires 1 numeric argument", + "=FISHER()": {"#VALUE!", "FISHER requires 1 numeric argument"}, + "=FISHER(2)": {"#N/A", "#N/A"}, + "=FISHER(\"2\")": {"#N/A", "#N/A"}, + "=FISHER(INT(-2)))": {"#N/A", "#N/A"}, + "=FISHER(F1)": {"#VALUE!", "FISHER requires 1 numeric argument"}, // FISHERINV - "=FISHERINV()": "FISHERINV requires 1 numeric argument", - "=FISHERINV(F1)": "FISHERINV requires 1 numeric argument", + "=FISHERINV()": {"#VALUE!", "FISHERINV requires 1 numeric argument"}, + "=FISHERINV(F1)": {"#VALUE!", "FISHERINV requires 1 numeric argument"}, // GAMMA - "=GAMMA()": "GAMMA requires 1 numeric argument", - "=GAMMA(F1)": "GAMMA requires 1 numeric argument", - "=GAMMA(0)": "#N/A", - "=GAMMA(\"0\")": "#N/A", - "=GAMMA(INT(0))": "#N/A", + "=GAMMA()": {"#VALUE!", "GAMMA requires 1 numeric argument"}, + "=GAMMA(F1)": {"#VALUE!", "GAMMA requires 1 numeric argument"}, + "=GAMMA(0)": {"#N/A", "#N/A"}, + "=GAMMA(\"0\")": {"#N/A", "#N/A"}, + "=GAMMA(INT(0))": {"#N/A", "#N/A"}, // GAMMA.DIST - "=GAMMA.DIST()": "GAMMA.DIST requires 4 arguments", - "=GAMMA.DIST(\"\",3,2,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMA.DIST(6,\"\",2,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMA.DIST(6,3,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMA.DIST(6,3,2,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=GAMMA.DIST(-1,3,2,FALSE)": "#NUM!", - "=GAMMA.DIST(6,0,2,FALSE)": "#NUM!", - "=GAMMA.DIST(6,3,0,FALSE)": "#NUM!", + "=GAMMA.DIST()": {"#VALUE!", "GAMMA.DIST requires 4 arguments"}, + "=GAMMA.DIST(\"\",3,2,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMA.DIST(6,\"\",2,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMA.DIST(6,3,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMA.DIST(6,3,2,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=GAMMA.DIST(-1,3,2,FALSE)": {"#NUM!", "#NUM!"}, + "=GAMMA.DIST(6,0,2,FALSE)": {"#NUM!", "#NUM!"}, + "=GAMMA.DIST(6,3,0,FALSE)": {"#NUM!", "#NUM!"}, // GAMMADIST - "=GAMMADIST()": "GAMMADIST requires 4 arguments", - "=GAMMADIST(\"\",3,2,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMADIST(6,\"\",2,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMADIST(6,3,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMADIST(6,3,2,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=GAMMADIST(-1,3,2,FALSE)": "#NUM!", - "=GAMMADIST(6,0,2,FALSE)": "#NUM!", - "=GAMMADIST(6,3,0,FALSE)": "#NUM!", + "=GAMMADIST()": {"#VALUE!", "GAMMADIST requires 4 arguments"}, + "=GAMMADIST(\"\",3,2,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMADIST(6,\"\",2,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMADIST(6,3,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMADIST(6,3,2,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=GAMMADIST(-1,3,2,FALSE)": {"#NUM!", "#NUM!"}, + "=GAMMADIST(6,0,2,FALSE)": {"#NUM!", "#NUM!"}, + "=GAMMADIST(6,3,0,FALSE)": {"#NUM!", "#NUM!"}, // GAMMA.INV - "=GAMMA.INV()": "GAMMA.INV requires 3 arguments", - "=GAMMA.INV(\"\",3,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMA.INV(0.5,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMA.INV(0.5,3,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMA.INV(-1,3,2)": "#NUM!", - "=GAMMA.INV(2,3,2)": "#NUM!", - "=GAMMA.INV(0.5,0,2)": "#NUM!", - "=GAMMA.INV(0.5,3,0)": "#NUM!", + "=GAMMA.INV()": {"#VALUE!", "GAMMA.INV requires 3 arguments"}, + "=GAMMA.INV(\"\",3,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMA.INV(0.5,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMA.INV(0.5,3,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMA.INV(-1,3,2)": {"#NUM!", "#NUM!"}, + "=GAMMA.INV(2,3,2)": {"#NUM!", "#NUM!"}, + "=GAMMA.INV(0.5,0,2)": {"#NUM!", "#NUM!"}, + "=GAMMA.INV(0.5,3,0)": {"#NUM!", "#NUM!"}, // GAMMAINV - "=GAMMAINV()": "GAMMAINV requires 3 arguments", - "=GAMMAINV(\"\",3,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMAINV(0.5,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMAINV(0.5,3,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMAINV(-1,3,2)": "#NUM!", - "=GAMMAINV(2,3,2)": "#NUM!", - "=GAMMAINV(0.5,0,2)": "#NUM!", - "=GAMMAINV(0.5,3,0)": "#NUM!", + "=GAMMAINV()": {"#VALUE!", "GAMMAINV requires 3 arguments"}, + "=GAMMAINV(\"\",3,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMAINV(0.5,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMAINV(0.5,3,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMAINV(-1,3,2)": {"#NUM!", "#NUM!"}, + "=GAMMAINV(2,3,2)": {"#NUM!", "#NUM!"}, + "=GAMMAINV(0.5,0,2)": {"#NUM!", "#NUM!"}, + "=GAMMAINV(0.5,3,0)": {"#NUM!", "#NUM!"}, // GAMMALN - "=GAMMALN()": "GAMMALN requires 1 numeric argument", - "=GAMMALN(F1)": "GAMMALN requires 1 numeric argument", - "=GAMMALN(0)": "#N/A", - "=GAMMALN(INT(0))": "#N/A", + "=GAMMALN()": {"#VALUE!", "GAMMALN requires 1 numeric argument"}, + "=GAMMALN(F1)": {"#VALUE!", "GAMMALN requires 1 numeric argument"}, + "=GAMMALN(0)": {"#N/A", "#N/A"}, + "=GAMMALN(INT(0))": {"#N/A", "#N/A"}, // GAMMALN.PRECISE - "=GAMMALN.PRECISE()": "GAMMALN.PRECISE requires 1 numeric argument", - "=GAMMALN.PRECISE(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=GAMMALN.PRECISE(0)": "#NUM!", + "=GAMMALN.PRECISE()": {"#VALUE!", "GAMMALN.PRECISE requires 1 numeric argument"}, + "=GAMMALN.PRECISE(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=GAMMALN.PRECISE(0)": {"#NUM!", "#NUM!"}, // GAUSS - "=GAUSS()": "GAUSS requires 1 numeric argument", - "=GAUSS(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=GAUSS()": {"#VALUE!", "GAUSS requires 1 numeric argument"}, + "=GAUSS(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // GEOMEAN - "=GEOMEAN()": "GEOMEAN requires at least 1 numeric argument", - "=GEOMEAN(0)": "#NUM!", - "=GEOMEAN(D1:D2)": "#NUM!", - "=GEOMEAN(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=GEOMEAN()": {"#VALUE!", "GEOMEAN requires at least 1 numeric argument"}, + "=GEOMEAN(0)": {"#NUM!", "#NUM!"}, + "=GEOMEAN(D1:D2)": {"#NUM!", "#NUM!"}, + "=GEOMEAN(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // HARMEAN - "=HARMEAN()": "HARMEAN requires at least 1 argument", - "=HARMEAN(-1)": "#N/A", - "=HARMEAN(0)": "#N/A", + "=HARMEAN()": {"#VALUE!", "HARMEAN requires at least 1 argument"}, + "=HARMEAN(-1)": {"#N/A", "#N/A"}, + "=HARMEAN(0)": {"#N/A", "#N/A"}, // HYPGEOM.DIST - "=HYPGEOM.DIST()": "HYPGEOM.DIST requires 5 arguments", - "=HYPGEOM.DIST(\"\",4,4,12,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=HYPGEOM.DIST(1,\"\",4,12,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=HYPGEOM.DIST(1,4,\"\",12,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=HYPGEOM.DIST(1,4,4,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=HYPGEOM.DIST(1,4,4,12,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=HYPGEOM.DIST(-1,4,4,12,FALSE)": "#NUM!", - "=HYPGEOM.DIST(2,1,4,12,FALSE)": "#NUM!", - "=HYPGEOM.DIST(2,4,1,12,FALSE)": "#NUM!", - "=HYPGEOM.DIST(2,2,2,1,FALSE)": "#NUM!", - "=HYPGEOM.DIST(1,0,4,12,FALSE)": "#NUM!", - "=HYPGEOM.DIST(1,4,4,2,FALSE)": "#NUM!", - "=HYPGEOM.DIST(1,4,0,12,FALSE)": "#NUM!", - "=HYPGEOM.DIST(1,4,4,0,FALSE)": "#NUM!", + "=HYPGEOM.DIST()": {"#VALUE!", "HYPGEOM.DIST requires 5 arguments"}, + "=HYPGEOM.DIST(\"\",4,4,12,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=HYPGEOM.DIST(1,\"\",4,12,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=HYPGEOM.DIST(1,4,\"\",12,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=HYPGEOM.DIST(1,4,4,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=HYPGEOM.DIST(1,4,4,12,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=HYPGEOM.DIST(-1,4,4,12,FALSE)": {"#NUM!", "#NUM!"}, + "=HYPGEOM.DIST(2,1,4,12,FALSE)": {"#NUM!", "#NUM!"}, + "=HYPGEOM.DIST(2,4,1,12,FALSE)": {"#NUM!", "#NUM!"}, + "=HYPGEOM.DIST(2,2,2,1,FALSE)": {"#NUM!", "#NUM!"}, + "=HYPGEOM.DIST(1,0,4,12,FALSE)": {"#NUM!", "#NUM!"}, + "=HYPGEOM.DIST(1,4,4,2,FALSE)": {"#NUM!", "#NUM!"}, + "=HYPGEOM.DIST(1,4,0,12,FALSE)": {"#NUM!", "#NUM!"}, + "=HYPGEOM.DIST(1,4,4,0,FALSE)": {"#NUM!", "#NUM!"}, // HYPGEOMDIST - "=HYPGEOMDIST()": "HYPGEOMDIST requires 4 numeric arguments", - "=HYPGEOMDIST(\"\",4,4,12)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=HYPGEOMDIST(1,\"\",4,12)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=HYPGEOMDIST(1,4,\"\",12)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=HYPGEOMDIST(1,4,4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=HYPGEOMDIST(-1,4,4,12)": "#NUM!", - "=HYPGEOMDIST(2,1,4,12)": "#NUM!", - "=HYPGEOMDIST(2,4,1,12)": "#NUM!", - "=HYPGEOMDIST(2,2,2,1)": "#NUM!", - "=HYPGEOMDIST(1,0,4,12)": "#NUM!", - "=HYPGEOMDIST(1,4,4,2)": "#NUM!", - "=HYPGEOMDIST(1,4,0,12)": "#NUM!", - "=HYPGEOMDIST(1,4,4,0)": "#NUM!", + "=HYPGEOMDIST()": {"#VALUE!", "HYPGEOMDIST requires 4 numeric arguments"}, + "=HYPGEOMDIST(\"\",4,4,12)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=HYPGEOMDIST(1,\"\",4,12)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=HYPGEOMDIST(1,4,\"\",12)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=HYPGEOMDIST(1,4,4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=HYPGEOMDIST(-1,4,4,12)": {"#NUM!", "#NUM!"}, + "=HYPGEOMDIST(2,1,4,12)": {"#NUM!", "#NUM!"}, + "=HYPGEOMDIST(2,4,1,12)": {"#NUM!", "#NUM!"}, + "=HYPGEOMDIST(2,2,2,1)": {"#NUM!", "#NUM!"}, + "=HYPGEOMDIST(1,0,4,12)": {"#NUM!", "#NUM!"}, + "=HYPGEOMDIST(1,4,4,2)": {"#NUM!", "#NUM!"}, + "=HYPGEOMDIST(1,4,0,12)": {"#NUM!", "#NUM!"}, + "=HYPGEOMDIST(1,4,4,0)": {"#NUM!", "#NUM!"}, // KURT - "=KURT()": "KURT requires at least 1 argument", - "=KURT(F1,INT(1))": "#DIV/0!", + "=KURT()": {"#VALUE!", "KURT requires at least 1 argument"}, + "=KURT(F1,INT(1))": {"#DIV/0!", "#DIV/0!"}, // EXPON.DIST - "=EXPON.DIST()": "EXPON.DIST requires 3 arguments", - "=EXPON.DIST(\"\",1,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=EXPON.DIST(0,\"\",TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=EXPON.DIST(0,1,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=EXPON.DIST(-1,1,TRUE)": "#NUM!", - "=EXPON.DIST(1,0,TRUE)": "#NUM!", + "=EXPON.DIST()": {"#VALUE!", "EXPON.DIST requires 3 arguments"}, + "=EXPON.DIST(\"\",1,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=EXPON.DIST(0,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=EXPON.DIST(0,1,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=EXPON.DIST(-1,1,TRUE)": {"#NUM!", "#NUM!"}, + "=EXPON.DIST(1,0,TRUE)": {"#NUM!", "#NUM!"}, // EXPONDIST - "=EXPONDIST()": "EXPONDIST requires 3 arguments", - "=EXPONDIST(\"\",1,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=EXPONDIST(0,\"\",TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=EXPONDIST(0,1,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=EXPONDIST(-1,1,TRUE)": "#NUM!", - "=EXPONDIST(1,0,TRUE)": "#NUM!", + "=EXPONDIST()": {"#VALUE!", "EXPONDIST requires 3 arguments"}, + "=EXPONDIST(\"\",1,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=EXPONDIST(0,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=EXPONDIST(0,1,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=EXPONDIST(-1,1,TRUE)": {"#NUM!", "#NUM!"}, + "=EXPONDIST(1,0,TRUE)": {"#NUM!", "#NUM!"}, // FDIST - "=FDIST()": "FDIST requires 3 arguments", - "=FDIST(\"\",1,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FDIST(5,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FDIST(5,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FDIST(-1,1,2)": "#NUM!", - "=FDIST(5,0,2)": "#NUM!", - "=FDIST(5,10000000000,2)": "#NUM!", - "=FDIST(5,1,0)": "#NUM!", - "=FDIST(5,1,10000000000)": "#NUM!", + "=FDIST()": {"#VALUE!", "FDIST requires 3 arguments"}, + "=FDIST(\"\",1,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FDIST(5,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FDIST(5,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FDIST(-1,1,2)": {"#NUM!", "#NUM!"}, + "=FDIST(5,0,2)": {"#NUM!", "#NUM!"}, + "=FDIST(5,10000000000,2)": {"#NUM!", "#NUM!"}, + "=FDIST(5,1,0)": {"#NUM!", "#NUM!"}, + "=FDIST(5,1,10000000000)": {"#NUM!", "#NUM!"}, // F.DIST - "=F.DIST()": "F.DIST requires 4 arguments", - "=F.DIST(\"\",2,5,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.DIST(1,\"\",5,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.DIST(1,2,\"\",TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.DIST(1,2,5,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=F.DIST(-1,1,2,TRUE)": "#NUM!", - "=F.DIST(5,0,2,TRUE)": "#NUM!", - "=F.DIST(5,10000000000,2,TRUE)": "#NUM!", - "=F.DIST(5,1,0,TRUE)": "#NUM!", - "=F.DIST(5,1,10000000000,TRUE)": "#NUM!", + "=F.DIST()": {"#VALUE!", "F.DIST requires 4 arguments"}, + "=F.DIST(\"\",2,5,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.DIST(1,\"\",5,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.DIST(1,2,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.DIST(1,2,5,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=F.DIST(-1,1,2,TRUE)": {"#NUM!", "#NUM!"}, + "=F.DIST(5,0,2,TRUE)": {"#NUM!", "#NUM!"}, + "=F.DIST(5,10000000000,2,TRUE)": {"#NUM!", "#NUM!"}, + "=F.DIST(5,1,0,TRUE)": {"#NUM!", "#NUM!"}, + "=F.DIST(5,1,10000000000,TRUE)": {"#NUM!", "#NUM!"}, // F.DIST.RT - "=F.DIST.RT()": "F.DIST.RT requires 3 arguments", - "=F.DIST.RT(\"\",1,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.DIST.RT(5,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.DIST.RT(5,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.DIST.RT(-1,1,2)": "#NUM!", - "=F.DIST.RT(5,0,2)": "#NUM!", - "=F.DIST.RT(5,10000000000,2)": "#NUM!", - "=F.DIST.RT(5,1,0)": "#NUM!", - "=F.DIST.RT(5,1,10000000000)": "#NUM!", + "=F.DIST.RT()": {"#VALUE!", "F.DIST.RT requires 3 arguments"}, + "=F.DIST.RT(\"\",1,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.DIST.RT(5,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.DIST.RT(5,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.DIST.RT(-1,1,2)": {"#NUM!", "#NUM!"}, + "=F.DIST.RT(5,0,2)": {"#NUM!", "#NUM!"}, + "=F.DIST.RT(5,10000000000,2)": {"#NUM!", "#NUM!"}, + "=F.DIST.RT(5,1,0)": {"#NUM!", "#NUM!"}, + "=F.DIST.RT(5,1,10000000000)": {"#NUM!", "#NUM!"}, // F.INV - "=F.INV()": "F.INV requires 3 arguments", - "=F.INV(\"\",1,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.INV(0.2,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.INV(0.2,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.INV(0,1,2)": "#NUM!", - "=F.INV(0.2,0.5,2)": "#NUM!", - "=F.INV(0.2,1,0.5)": "#NUM!", + "=F.INV()": {"#VALUE!", "F.INV requires 3 arguments"}, + "=F.INV(\"\",1,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.INV(0.2,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.INV(0.2,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.INV(0,1,2)": {"#NUM!", "#NUM!"}, + "=F.INV(0.2,0.5,2)": {"#NUM!", "#NUM!"}, + "=F.INV(0.2,1,0.5)": {"#NUM!", "#NUM!"}, // FINV - "=FINV()": "FINV requires 3 arguments", - "=FINV(\"\",1,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FINV(0.2,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FINV(0.2,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FINV(0,1,2)": "#NUM!", - "=FINV(0.2,0.5,2)": "#NUM!", - "=FINV(0.2,1,0.5)": "#NUM!", + "=FINV()": {"#VALUE!", "FINV requires 3 arguments"}, + "=FINV(\"\",1,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FINV(0.2,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FINV(0.2,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FINV(0,1,2)": {"#NUM!", "#NUM!"}, + "=FINV(0.2,0.5,2)": {"#NUM!", "#NUM!"}, + "=FINV(0.2,1,0.5)": {"#NUM!", "#NUM!"}, // F.INV.RT - "=F.INV.RT()": "F.INV.RT requires 3 arguments", - "=F.INV.RT(\"\",1,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.INV.RT(0.2,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.INV.RT(0.2,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=F.INV.RT(0,1,2)": "#NUM!", - "=F.INV.RT(0.2,0.5,2)": "#NUM!", - "=F.INV.RT(0.2,1,0.5)": "#NUM!", + "=F.INV.RT()": {"#VALUE!", "F.INV.RT requires 3 arguments"}, + "=F.INV.RT(\"\",1,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.INV.RT(0.2,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.INV.RT(0.2,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=F.INV.RT(0,1,2)": {"#NUM!", "#NUM!"}, + "=F.INV.RT(0.2,0.5,2)": {"#NUM!", "#NUM!"}, + "=F.INV.RT(0.2,1,0.5)": {"#NUM!", "#NUM!"}, // LOGINV - "=LOGINV()": "LOGINV requires 3 arguments", - "=LOGINV(\"\",2,0.2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGINV(0.3,\"\",0.2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGINV(0.3,2,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGINV(0,2,0.2)": "#NUM!", - "=LOGINV(1,2,0.2)": "#NUM!", - "=LOGINV(0.3,2,0)": "#NUM!", + "=LOGINV()": {"#VALUE!", "LOGINV requires 3 arguments"}, + "=LOGINV(\"\",2,0.2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGINV(0.3,\"\",0.2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGINV(0.3,2,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGINV(0,2,0.2)": {"#NUM!", "#NUM!"}, + "=LOGINV(1,2,0.2)": {"#NUM!", "#NUM!"}, + "=LOGINV(0.3,2,0)": {"#NUM!", "#NUM!"}, // LOGNORM.INV - "=LOGNORM.INV()": "LOGNORM.INV requires 3 arguments", - "=LOGNORM.INV(\"\",2,0.2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGNORM.INV(0.3,\"\",0.2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGNORM.INV(0.3,2,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGNORM.INV(0,2,0.2)": "#NUM!", - "=LOGNORM.INV(1,2,0.2)": "#NUM!", - "=LOGNORM.INV(0.3,2,0)": "#NUM!", + "=LOGNORM.INV()": {"#VALUE!", "LOGNORM.INV requires 3 arguments"}, + "=LOGNORM.INV(\"\",2,0.2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGNORM.INV(0.3,\"\",0.2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGNORM.INV(0.3,2,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGNORM.INV(0,2,0.2)": {"#NUM!", "#NUM!"}, + "=LOGNORM.INV(1,2,0.2)": {"#NUM!", "#NUM!"}, + "=LOGNORM.INV(0.3,2,0)": {"#NUM!", "#NUM!"}, // LOGNORM.DIST - "=LOGNORM.DIST()": "LOGNORM.DIST requires 4 arguments", - "=LOGNORM.DIST(\"\",10,5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGNORM.DIST(0.5,\"\",5,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGNORM.DIST(0.5,10,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGNORM.DIST(0.5,10,5,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=LOGNORM.DIST(0,10,5,FALSE)": "#NUM!", - "=LOGNORM.DIST(0.5,10,0,FALSE)": "#NUM!", + "=LOGNORM.DIST()": {"#VALUE!", "LOGNORM.DIST requires 4 arguments"}, + "=LOGNORM.DIST(\"\",10,5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGNORM.DIST(0.5,\"\",5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGNORM.DIST(0.5,10,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGNORM.DIST(0.5,10,5,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=LOGNORM.DIST(0,10,5,FALSE)": {"#NUM!", "#NUM!"}, + "=LOGNORM.DIST(0.5,10,0,FALSE)": {"#NUM!", "#NUM!"}, // LOGNORMDIST - "=LOGNORMDIST()": "LOGNORMDIST requires 3 arguments", - "=LOGNORMDIST(\"\",10,5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGNORMDIST(12,\"\",5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGNORMDIST(12,10,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LOGNORMDIST(0,2,5)": "#NUM!", - "=LOGNORMDIST(12,10,0)": "#NUM!", + "=LOGNORMDIST()": {"#VALUE!", "LOGNORMDIST requires 3 arguments"}, + "=LOGNORMDIST(\"\",10,5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGNORMDIST(12,\"\",5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGNORMDIST(12,10,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LOGNORMDIST(0,2,5)": {"#NUM!", "#NUM!"}, + "=LOGNORMDIST(12,10,0)": {"#NUM!", "#NUM!"}, // NEGBINOM.DIST - "=NEGBINOM.DIST()": "NEGBINOM.DIST requires 4 arguments", - "=NEGBINOM.DIST(\"\",12,0.5,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NEGBINOM.DIST(6,\"\",0.5,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NEGBINOM.DIST(6,12,\"\",TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NEGBINOM.DIST(6,12,0.5,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=NEGBINOM.DIST(-1,12,0.5,TRUE)": "#NUM!", - "=NEGBINOM.DIST(6,0,0.5,TRUE)": "#NUM!", - "=NEGBINOM.DIST(6,12,-1,TRUE)": "#NUM!", - "=NEGBINOM.DIST(6,12,2,TRUE)": "#NUM!", + "=NEGBINOM.DIST()": {"#VALUE!", "NEGBINOM.DIST requires 4 arguments"}, + "=NEGBINOM.DIST(\"\",12,0.5,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NEGBINOM.DIST(6,\"\",0.5,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NEGBINOM.DIST(6,12,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NEGBINOM.DIST(6,12,0.5,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=NEGBINOM.DIST(-1,12,0.5,TRUE)": {"#NUM!", "#NUM!"}, + "=NEGBINOM.DIST(6,0,0.5,TRUE)": {"#NUM!", "#NUM!"}, + "=NEGBINOM.DIST(6,12,-1,TRUE)": {"#NUM!", "#NUM!"}, + "=NEGBINOM.DIST(6,12,2,TRUE)": {"#NUM!", "#NUM!"}, // NEGBINOMDIST - "=NEGBINOMDIST()": "NEGBINOMDIST requires 3 arguments", - "=NEGBINOMDIST(\"\",12,0.5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NEGBINOMDIST(6,\"\",0.5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NEGBINOMDIST(6,12,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NEGBINOMDIST(-1,12,0.5)": "#NUM!", - "=NEGBINOMDIST(6,0,0.5)": "#NUM!", - "=NEGBINOMDIST(6,12,-1)": "#NUM!", - "=NEGBINOMDIST(6,12,2)": "#NUM!", + "=NEGBINOMDIST()": {"#VALUE!", "NEGBINOMDIST requires 3 arguments"}, + "=NEGBINOMDIST(\"\",12,0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NEGBINOMDIST(6,\"\",0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NEGBINOMDIST(6,12,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NEGBINOMDIST(-1,12,0.5)": {"#NUM!", "#NUM!"}, + "=NEGBINOMDIST(6,0,0.5)": {"#NUM!", "#NUM!"}, + "=NEGBINOMDIST(6,12,-1)": {"#NUM!", "#NUM!"}, + "=NEGBINOMDIST(6,12,2)": {"#NUM!", "#NUM!"}, // NORM.DIST - "=NORM.DIST()": "NORM.DIST requires 4 arguments", + "=NORM.DIST()": {"#VALUE!", "NORM.DIST requires 4 arguments"}, // NORMDIST - "=NORMDIST()": "NORMDIST requires 4 arguments", - "=NORMDIST(\"\",0,0,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NORMDIST(0,\"\",0,FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NORMDIST(0,0,\"\",FALSE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NORMDIST(0,0,0,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=NORMDIST(0,0,-1,TRUE)": "#N/A", + "=NORMDIST()": {"#VALUE!", "NORMDIST requires 4 arguments"}, + "=NORMDIST(\"\",0,0,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NORMDIST(0,\"\",0,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NORMDIST(0,0,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NORMDIST(0,0,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=NORMDIST(0,0,-1,TRUE)": {"#N/A", "#N/A"}, // NORM.INV - "=NORM.INV()": "NORM.INV requires 3 arguments", + "=NORM.INV()": {"#VALUE!", "NORM.INV requires 3 arguments"}, // NORMINV - "=NORMINV()": "NORMINV requires 3 arguments", - "=NORMINV(\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NORMINV(0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NORMINV(0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NORMINV(0,0,-1)": "#N/A", - "=NORMINV(-1,0,0)": "#N/A", - "=NORMINV(0,0,0)": "#NUM!", + "=NORMINV()": {"#VALUE!", "NORMINV requires 3 arguments"}, + "=NORMINV(\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NORMINV(0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NORMINV(0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NORMINV(0,0,-1)": {"#N/A", "#N/A"}, + "=NORMINV(-1,0,0)": {"#N/A", "#N/A"}, + "=NORMINV(0,0,0)": {"#NUM!", "#NUM!"}, // NORM.S.DIST - "=NORM.S.DIST()": "NORM.S.DIST requires 2 numeric arguments", + "=NORM.S.DIST()": {"#VALUE!", "NORM.S.DIST requires 2 numeric arguments"}, // NORMSDIST - "=NORMSDIST()": "NORMSDIST requires 1 numeric argument", + "=NORMSDIST()": {"#VALUE!", "NORMSDIST requires 1 numeric argument"}, // NORM.S.INV - "=NORM.S.INV()": "NORM.S.INV requires 1 numeric argument", + "=NORM.S.INV()": {"#VALUE!", "NORM.S.INV requires 1 numeric argument"}, // NORMSINV - "=NORMSINV()": "NORMSINV requires 1 numeric argument", + "=NORMSINV()": {"#VALUE!", "NORMSINV requires 1 numeric argument"}, // LARGE - "=LARGE()": "LARGE requires 2 arguments", - "=LARGE(A1:A5,0)": "k should be > 0", - "=LARGE(A1:A5,6)": "k should be <= length of array", - "=LARGE(A1:A5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=LARGE()": {"#VALUE!", "LARGE requires 2 arguments"}, + "=LARGE(A1:A5,0)": {"#NUM!", "k should be > 0"}, + "=LARGE(A1:A5,6)": {"#NUM!", "k should be <= length of array"}, + "=LARGE(A1:A5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // MAX - "=MAX()": "MAX requires at least 1 argument", - "=MAX(NA())": "#N/A", + "=MAX()": {"#VALUE!", "MAX requires at least 1 argument"}, + "=MAX(NA())": {"#N/A", "#N/A"}, // MAXA - "=MAXA()": "MAXA requires at least 1 argument", - "=MAXA(NA())": "#N/A", + "=MAXA()": {"#VALUE!", "MAXA requires at least 1 argument"}, + "=MAXA(NA())": {"#N/A", "#N/A"}, // MAXIFS - "=MAXIFS()": "MAXIFS requires at least 3 arguments", - "=MAXIFS(F2:F4,A2:A4,\">0\",D2:D9)": "#N/A", + "=MAXIFS()": {"#VALUE!", "MAXIFS requires at least 3 arguments"}, + "=MAXIFS(F2:F4,A2:A4,\">0\",D2:D9)": {"#N/A", "#N/A"}, // MEDIAN - "=MEDIAN()": "MEDIAN requires at least 1 argument", - "=MEDIAN(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MEDIAN(D1:D2)": "#NUM!", + "=MEDIAN()": {"#VALUE!", "MEDIAN requires at least 1 argument"}, + "=MEDIAN(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=MEDIAN(D1:D2)": {"#NUM!", "#NUM!"}, // MIN - "=MIN()": "MIN requires at least 1 argument", - "=MIN(NA())": "#N/A", + "=MIN()": {"#VALUE!", "MIN requires at least 1 argument"}, + "=MIN(NA())": {"#N/A", "#N/A"}, // MINA - "=MINA()": "MINA requires at least 1 argument", - "=MINA(NA())": "#N/A", + "=MINA()": {"#VALUE!", "MINA requires at least 1 argument"}, + "=MINA(NA())": {"#N/A", "#N/A"}, // MINIFS - "=MINIFS()": "MINIFS requires at least 3 arguments", - "=MINIFS(F2:F4,A2:A4,\"<0\",D2:D9)": "#N/A", + "=MINIFS()": {"#VALUE!", "MINIFS requires at least 3 arguments"}, + "=MINIFS(F2:F4,A2:A4,\"<0\",D2:D9)": {"#N/A", "#N/A"}, // PEARSON - "=PEARSON()": "PEARSON requires 2 arguments", - "=PEARSON(A1:A2,B1:B1)": "#N/A", - "=PEARSON(A4,A4)": "#DIV/0!", + "=PEARSON()": {"#VALUE!", "PEARSON requires 2 arguments"}, + "=PEARSON(A1:A2,B1:B1)": {"#N/A", "#N/A"}, + "=PEARSON(A4,A4)": {"#DIV/0!", "#DIV/0!"}, // PERCENTILE.EXC - "=PERCENTILE.EXC()": "PERCENTILE.EXC requires 2 arguments", - "=PERCENTILE.EXC(A1:A4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERCENTILE.EXC(A1:A4,-1)": "#NUM!", - "=PERCENTILE.EXC(A1:A4,0)": "#NUM!", - "=PERCENTILE.EXC(A1:A4,1)": "#NUM!", - "=PERCENTILE.EXC(NA(),0.5)": "#NUM!", + "=PERCENTILE.EXC()": {"#VALUE!", "PERCENTILE.EXC requires 2 arguments"}, + "=PERCENTILE.EXC(A1:A4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERCENTILE.EXC(A1:A4,-1)": {"#NUM!", "#NUM!"}, + "=PERCENTILE.EXC(A1:A4,0)": {"#NUM!", "#NUM!"}, + "=PERCENTILE.EXC(A1:A4,1)": {"#NUM!", "#NUM!"}, + "=PERCENTILE.EXC(NA(),0.5)": {"#NUM!", "#NUM!"}, // PERCENTILE.INC - "=PERCENTILE.INC()": "PERCENTILE.INC requires 2 arguments", + "=PERCENTILE.INC()": {"#VALUE!", "PERCENTILE.INC requires 2 arguments"}, // PERCENTILE - "=PERCENTILE()": "PERCENTILE requires 2 arguments", - "=PERCENTILE(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERCENTILE(0,-1)": "#N/A", - "=PERCENTILE(NA(),1)": "#N/A", + "=PERCENTILE()": {"#VALUE!", "PERCENTILE requires 2 arguments"}, + "=PERCENTILE(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERCENTILE(0,-1)": {"#N/A", "#N/A"}, + "=PERCENTILE(NA(),1)": {"#N/A", "#N/A"}, // PERCENTRANK.EXC - "=PERCENTRANK.EXC()": "PERCENTRANK.EXC requires 2 or 3 arguments", - "=PERCENTRANK.EXC(A1:B4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERCENTRANK.EXC(A1:B4,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERCENTRANK.EXC(A1:B4,0,0)": "PERCENTRANK.EXC arguments significance should be > 1", - "=PERCENTRANK.EXC(A1:B4,6)": "#N/A", - "=PERCENTRANK.EXC(NA(),1)": "#N/A", + "=PERCENTRANK.EXC()": {"#VALUE!", "PERCENTRANK.EXC requires 2 or 3 arguments"}, + "=PERCENTRANK.EXC(A1:B4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERCENTRANK.EXC(A1:B4,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERCENTRANK.EXC(A1:B4,0,0)": {"#NUM!", "PERCENTRANK.EXC arguments significance should be > 1"}, + "=PERCENTRANK.EXC(A1:B4,6)": {"#N/A", "#N/A"}, + "=PERCENTRANK.EXC(NA(),1)": {"#N/A", "#N/A"}, // PERCENTRANK.INC - "=PERCENTRANK.INC()": "PERCENTRANK.INC requires 2 or 3 arguments", - "=PERCENTRANK.INC(A1:B4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERCENTRANK.INC(A1:B4,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERCENTRANK.INC(A1:B4,0,0)": "PERCENTRANK.INC arguments significance should be > 1", - "=PERCENTRANK.INC(A1:B4,6)": "#N/A", - "=PERCENTRANK.INC(NA(),1)": "#N/A", + "=PERCENTRANK.INC()": {"#VALUE!", "PERCENTRANK.INC requires 2 or 3 arguments"}, + "=PERCENTRANK.INC(A1:B4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERCENTRANK.INC(A1:B4,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERCENTRANK.INC(A1:B4,0,0)": {"#NUM!", "PERCENTRANK.INC arguments significance should be > 1"}, + "=PERCENTRANK.INC(A1:B4,6)": {"#N/A", "#N/A"}, + "=PERCENTRANK.INC(NA(),1)": {"#N/A", "#N/A"}, // PERCENTRANK - "=PERCENTRANK()": "PERCENTRANK requires 2 or 3 arguments", - "=PERCENTRANK(A1:B4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERCENTRANK(A1:B4,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERCENTRANK(A1:B4,0,0)": "PERCENTRANK arguments significance should be > 1", - "=PERCENTRANK(A1:B4,6)": "#N/A", - "=PERCENTRANK(NA(),1)": "#N/A", + "=PERCENTRANK()": {"#VALUE!", "PERCENTRANK requires 2 or 3 arguments"}, + "=PERCENTRANK(A1:B4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERCENTRANK(A1:B4,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERCENTRANK(A1:B4,0,0)": {"#NUM!", "PERCENTRANK arguments significance should be > 1"}, + "=PERCENTRANK(A1:B4,6)": {"#N/A", "#N/A"}, + "=PERCENTRANK(NA(),1)": {"#N/A", "#N/A"}, // PERMUT - "=PERMUT()": "PERMUT requires 2 numeric arguments", - "=PERMUT(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERMUT(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERMUT(6,8)": "#N/A", + "=PERMUT()": {"#VALUE!", "PERMUT requires 2 numeric arguments"}, + "=PERMUT(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERMUT(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERMUT(6,8)": {"#N/A", "#N/A"}, // PERMUTATIONA - "=PERMUTATIONA()": "PERMUTATIONA requires 2 numeric arguments", - "=PERMUTATIONA(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERMUTATIONA(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PERMUTATIONA(-1,0)": "#N/A", - "=PERMUTATIONA(0,-1)": "#N/A", + "=PERMUTATIONA()": {"#VALUE!", "PERMUTATIONA requires 2 numeric arguments"}, + "=PERMUTATIONA(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERMUTATIONA(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PERMUTATIONA(-1,0)": {"#N/A", "#N/A"}, + "=PERMUTATIONA(0,-1)": {"#N/A", "#N/A"}, // PHI - "=PHI()": "PHI requires 1 argument", - "=PHI(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=PHI()": {"#VALUE!", "PHI requires 1 argument"}, + "=PHI(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // QUARTILE - "=QUARTILE()": "QUARTILE requires 2 arguments", - "=QUARTILE(A1:A4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=QUARTILE(A1:A4,-1)": "#NUM!", - "=QUARTILE(A1:A4,5)": "#NUM!", + "=QUARTILE()": {"#VALUE!", "QUARTILE requires 2 arguments"}, + "=QUARTILE(A1:A4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=QUARTILE(A1:A4,-1)": {"#NUM!", "#NUM!"}, + "=QUARTILE(A1:A4,5)": {"#NUM!", "#NUM!"}, // QUARTILE.EXC - "=QUARTILE.EXC()": "QUARTILE.EXC requires 2 arguments", - "=QUARTILE.EXC(A1:A4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=QUARTILE.EXC(A1:A4,0)": "#NUM!", - "=QUARTILE.EXC(A1:A4,4)": "#NUM!", + "=QUARTILE.EXC()": {"#VALUE!", "QUARTILE.EXC requires 2 arguments"}, + "=QUARTILE.EXC(A1:A4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=QUARTILE.EXC(A1:A4,0)": {"#NUM!", "#NUM!"}, + "=QUARTILE.EXC(A1:A4,4)": {"#NUM!", "#NUM!"}, // QUARTILE.INC - "=QUARTILE.INC()": "QUARTILE.INC requires 2 arguments", + "=QUARTILE.INC()": {"#VALUE!", "QUARTILE.INC requires 2 arguments"}, // RANK - "=RANK()": "RANK requires at least 2 arguments", - "=RANK(1,A1:B5,0,0)": "RANK requires at most 3 arguments", - "=RANK(-1,A1:B5)": "#N/A", - "=RANK(\"\",A1:B5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=RANK(1,A1:B5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=RANK()": {"#VALUE!", "RANK requires at least 2 arguments"}, + "=RANK(1,A1:B5,0,0)": {"#VALUE!", "RANK requires at most 3 arguments"}, + "=RANK(-1,A1:B5)": {"#N/A", "#N/A"}, + "=RANK(\"\",A1:B5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=RANK(1,A1:B5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // RANK.EQ - "=RANK.EQ()": "RANK.EQ requires at least 2 arguments", - "=RANK.EQ(1,A1:B5,0,0)": "RANK.EQ requires at most 3 arguments", - "=RANK.EQ(-1,A1:B5)": "#N/A", - "=RANK.EQ(\"\",A1:B5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=RANK.EQ(1,A1:B5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=RANK.EQ()": {"#VALUE!", "RANK.EQ requires at least 2 arguments"}, + "=RANK.EQ(1,A1:B5,0,0)": {"#VALUE!", "RANK.EQ requires at most 3 arguments"}, + "=RANK.EQ(-1,A1:B5)": {"#N/A", "#N/A"}, + "=RANK.EQ(\"\",A1:B5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=RANK.EQ(1,A1:B5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // RSQ - "=RSQ()": "RSQ requires 2 arguments", - "=RSQ(A1:A2,B1:B1)": "#N/A", - "=RSQ(A4,A4)": "#DIV/0!", + "=RSQ()": {"#VALUE!", "RSQ requires 2 arguments"}, + "=RSQ(A1:A2,B1:B1)": {"#N/A", "#N/A"}, + "=RSQ(A4,A4)": {"#DIV/0!", "#DIV/0!"}, // SKEW - "=SKEW()": "SKEW requires at least 1 argument", - "=SKEW(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=SKEW(0)": "#DIV/0!", + "=SKEW()": {"#VALUE!", "SKEW requires at least 1 argument"}, + "=SKEW(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=SKEW(0)": {"#DIV/0!", "#DIV/0!"}, // SKEW.P - "=SKEW.P()": "SKEW.P requires at least 1 argument", - "=SKEW.P(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=SKEW.P(0)": "#DIV/0!", + "=SKEW.P()": {"#VALUE!", "SKEW.P requires at least 1 argument"}, + "=SKEW.P(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=SKEW.P(0)": {"#DIV/0!", "#DIV/0!"}, // SLOPE - "=SLOPE()": "SLOPE requires 2 arguments", - "=SLOPE(A1:A2,B1:B1)": "#N/A", - "=SLOPE(A4,A4)": "#DIV/0!", + "=SLOPE()": {"#VALUE!", "SLOPE requires 2 arguments"}, + "=SLOPE(A1:A2,B1:B1)": {"#N/A", "#N/A"}, + "=SLOPE(A4,A4)": {"#DIV/0!", "#DIV/0!"}, // SMALL - "=SMALL()": "SMALL requires 2 arguments", - "=SMALL(A1:A5,0)": "k should be > 0", - "=SMALL(A1:A5,6)": "k should be <= length of array", - "=SMALL(A1:A5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=SMALL()": {"#VALUE!", "SMALL requires 2 arguments"}, + "=SMALL(A1:A5,0)": {"#NUM!", "k should be > 0"}, + "=SMALL(A1:A5,6)": {"#NUM!", "k should be <= length of array"}, + "=SMALL(A1:A5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // STANDARDIZE - "=STANDARDIZE()": "STANDARDIZE requires 3 arguments", - "=STANDARDIZE(\"\",0,5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=STANDARDIZE(0,\"\",5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=STANDARDIZE(0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=STANDARDIZE(0,0,0)": "#N/A", + "=STANDARDIZE()": {"#VALUE!", "STANDARDIZE requires 3 arguments"}, + "=STANDARDIZE(\"\",0,5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=STANDARDIZE(0,\"\",5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=STANDARDIZE(0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=STANDARDIZE(0,0,0)": {"#N/A", "#N/A"}, // STDEVP - "=STDEVP()": "STDEVP requires at least 1 argument", - "=STDEVP(\"\")": "#DIV/0!", + "=STDEVP()": {"#VALUE!", "STDEVP requires at least 1 argument"}, + "=STDEVP(\"\")": {"#DIV/0!", "#DIV/0!"}, // STDEV.P - "=STDEV.P()": "STDEV.P requires at least 1 argument", - "=STDEV.P(\"\")": "#DIV/0!", + "=STDEV.P()": {"#VALUE!", "STDEV.P requires at least 1 argument"}, + "=STDEV.P(\"\")": {"#DIV/0!", "#DIV/0!"}, // STDEVPA - "=STDEVPA()": "STDEVPA requires at least 1 argument", - "=STDEVPA(\"\")": "#DIV/0!", + "=STDEVPA()": {"#VALUE!", "STDEVPA requires at least 1 argument"}, + "=STDEVPA(\"\")": {"#DIV/0!", "#DIV/0!"}, // T.DIST - "=T.DIST()": "T.DIST requires 3 arguments", - "=T.DIST(\"\",10,TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.DIST(1,\"\",TRUE)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.DIST(1,10,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=T.DIST(1,0,TRUE)": "#NUM!", - "=T.DIST(1,-1,FALSE)": "#NUM!", - "=T.DIST(1,0,FALSE)": "#DIV/0!", + "=T.DIST()": {"#VALUE!", "T.DIST requires 3 arguments"}, + "=T.DIST(\"\",10,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=T.DIST(1,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=T.DIST(1,10,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=T.DIST(1,0,TRUE)": {"#NUM!", "#NUM!"}, + "=T.DIST(1,-1,FALSE)": {"#NUM!", "#NUM!"}, + "=T.DIST(1,0,FALSE)": {"#DIV/0!", "#DIV/0!"}, // T.DIST.2T - "=T.DIST.2T()": "T.DIST.2T requires 2 arguments", - "=T.DIST.2T(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.DIST.2T(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.DIST.2T(-1,10)": "#NUM!", - "=T.DIST.2T(1,0)": "#NUM!", + "=T.DIST.2T()": {"#VALUE!", "T.DIST.2T requires 2 arguments"}, + "=T.DIST.2T(\"\",10)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=T.DIST.2T(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=T.DIST.2T(-1,10)": {"#NUM!", "#NUM!"}, + "=T.DIST.2T(1,0)": {"#NUM!", "#NUM!"}, // T.DIST.RT - "=T.DIST.RT()": "T.DIST.RT requires 2 arguments", - "=T.DIST.RT(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.DIST.RT(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.DIST.RT(1,0)": "#NUM!", + "=T.DIST.RT()": {"#VALUE!", "T.DIST.RT requires 2 arguments"}, + "=T.DIST.RT(\"\",10)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=T.DIST.RT(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=T.DIST.RT(1,0)": {"#NUM!", "#NUM!"}, // TDIST - "=TDIST()": "TDIST requires 3 arguments", - "=TDIST(\"\",10,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=TDIST(1,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=TDIST(1,10,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=TDIST(-1,10,1)": "#NUM!", - "=TDIST(1,0,1)": "#NUM!", - "=TDIST(1,10,0)": "#NUM!", + "=TDIST()": {"#VALUE!", "TDIST requires 3 arguments"}, + "=TDIST(\"\",10,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=TDIST(1,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=TDIST(1,10,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=TDIST(-1,10,1)": {"#NUM!", "#NUM!"}, + "=TDIST(1,0,1)": {"#NUM!", "#NUM!"}, + "=TDIST(1,10,0)": {"#NUM!", "#NUM!"}, // T.INV - "=T.INV()": "T.INV requires 2 arguments", - "=T.INV(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.INV(0.25,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.INV(0,10)": "#NUM!", - "=T.INV(1,10)": "#NUM!", - "=T.INV(0.25,0.5)": "#NUM!", + "=T.INV()": {"#VALUE!", "T.INV requires 2 arguments"}, + "=T.INV(\"\",10)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=T.INV(0.25,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=T.INV(0,10)": {"#NUM!", "#NUM!"}, + "=T.INV(1,10)": {"#NUM!", "#NUM!"}, + "=T.INV(0.25,0.5)": {"#NUM!", "#NUM!"}, // T.INV.2T - "=T.INV.2T()": "T.INV.2T requires 2 arguments", - "=T.INV.2T(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.INV.2T(0.25,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.INV.2T(0,10)": "#NUM!", - "=T.INV.2T(0.25,0.5)": "#NUM!", + "=T.INV.2T()": {"#VALUE!", "T.INV.2T requires 2 arguments"}, + "=T.INV.2T(\"\",10)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=T.INV.2T(0.25,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=T.INV.2T(0,10)": {"#NUM!", "#NUM!"}, + "=T.INV.2T(0.25,0.5)": {"#NUM!", "#NUM!"}, // TINV - "=TINV()": "TINV requires 2 arguments", - "=TINV(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=TINV(0.25,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=TINV(0,10)": "#NUM!", - "=TINV(0.25,0.5)": "#NUM!", + "=TINV()": {"#VALUE!", "TINV requires 2 arguments"}, + "=TINV(\"\",10)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=TINV(0.25,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=TINV(0,10)": {"#NUM!", "#NUM!"}, + "=TINV(0.25,0.5)": {"#NUM!", "#NUM!"}, // TRIMMEAN - "=TRIMMEAN()": "TRIMMEAN requires 2 arguments", - "=TRIMMEAN(A1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=TRIMMEAN(A1,1)": "#NUM!", - "=TRIMMEAN(A1,-1)": "#NUM!", + "=TRIMMEAN()": {"#VALUE!", "TRIMMEAN requires 2 arguments"}, + "=TRIMMEAN(A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=TRIMMEAN(A1,1)": {"#NUM!", "#NUM!"}, + "=TRIMMEAN(A1,-1)": {"#NUM!", "#NUM!"}, // VAR - "=VAR()": "VAR requires at least 1 argument", + "=VAR()": {"#VALUE!", "VAR requires at least 1 argument"}, // VARA - "=VARA()": "VARA requires at least 1 argument", + "=VARA()": {"#VALUE!", "VARA requires at least 1 argument"}, // VARP - "=VARP()": "VARP requires at least 1 argument", - "=VARP(\"\")": "#DIV/0!", + "=VARP()": {"#VALUE!", "VARP requires at least 1 argument"}, + "=VARP(\"\")": {"#DIV/0!", "#DIV/0!"}, // VAR.P - "=VAR.P()": "VAR.P requires at least 1 argument", - "=VAR.P(\"\")": "#DIV/0!", + "=VAR.P()": {"#VALUE!", "VAR.P requires at least 1 argument"}, + "=VAR.P(\"\")": {"#DIV/0!", "#DIV/0!"}, // VAR.S - "=VAR.S()": "VAR.S requires at least 1 argument", + "=VAR.S()": {"#VALUE!", "VAR.S requires at least 1 argument"}, // VARPA - "=VARPA()": "VARPA requires at least 1 argument", + "=VARPA()": {"#VALUE!", "VARPA requires at least 1 argument"}, // WEIBULL - "=WEIBULL()": "WEIBULL requires 4 arguments", - "=WEIBULL(\"\",1,1,FALSE)": "#VALUE!", - "=WEIBULL(1,0,1,FALSE)": "#N/A", - "=WEIBULL(1,1,-1,FALSE)": "#N/A", + "=WEIBULL()": {"#VALUE!", "WEIBULL requires 4 arguments"}, + "=WEIBULL(\"\",1,1,FALSE)": {"#VALUE!", "#VALUE!"}, + "=WEIBULL(1,0,1,FALSE)": {"#N/A", "#N/A"}, + "=WEIBULL(1,1,-1,FALSE)": {"#N/A", "#N/A"}, // WEIBULL.DIST - "=WEIBULL.DIST()": "WEIBULL.DIST requires 4 arguments", - "=WEIBULL.DIST(\"\",1,1,FALSE)": "#VALUE!", - "=WEIBULL.DIST(1,0,1,FALSE)": "#N/A", - "=WEIBULL.DIST(1,1,-1,FALSE)": "#N/A", + "=WEIBULL.DIST()": {"#VALUE!", "WEIBULL.DIST requires 4 arguments"}, + "=WEIBULL.DIST(\"\",1,1,FALSE)": {"#VALUE!", "#VALUE!"}, + "=WEIBULL.DIST(1,0,1,FALSE)": {"#N/A", "#N/A"}, + "=WEIBULL.DIST(1,1,-1,FALSE)": {"#N/A", "#N/A"}, // Z.TEST - "=Z.TEST(A1)": "Z.TEST requires at least 2 arguments", - "=Z.TEST(A1,0,0,0)": "Z.TEST accepts at most 3 arguments", - "=Z.TEST(H1,0)": "#N/A", - "=Z.TEST(A1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=Z.TEST(A1,1)": "#DIV/0!", - "=Z.TEST(A1,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=Z.TEST(A1)": {"#VALUE!", "Z.TEST requires at least 2 arguments"}, + "=Z.TEST(A1,0,0,0)": {"#VALUE!", "Z.TEST accepts at most 3 arguments"}, + "=Z.TEST(H1,0)": {"#N/A", "#N/A"}, + "=Z.TEST(A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=Z.TEST(A1,1)": {"#DIV/0!", "#DIV/0!"}, + "=Z.TEST(A1,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // ZTEST - "=ZTEST(A1)": "ZTEST requires at least 2 arguments", - "=ZTEST(A1,0,0,0)": "ZTEST accepts at most 3 arguments", - "=ZTEST(H1,0)": "#N/A", - "=ZTEST(A1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=ZTEST(A1,1)": "#DIV/0!", - "=ZTEST(A1,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=ZTEST(A1)": {"#VALUE!", "ZTEST requires at least 2 arguments"}, + "=ZTEST(A1,0,0,0)": {"#VALUE!", "ZTEST accepts at most 3 arguments"}, + "=ZTEST(H1,0)": {"#N/A", "#N/A"}, + "=ZTEST(A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ZTEST(A1,1)": {"#DIV/0!", "#DIV/0!"}, + "=ZTEST(A1,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // Information Functions // ERROR.TYPE - "=ERROR.TYPE()": "ERROR.TYPE requires 1 argument", - "=ERROR.TYPE(1)": "#N/A", + "=ERROR.TYPE()": {"#VALUE!", "ERROR.TYPE requires 1 argument"}, + "=ERROR.TYPE(1)": {"#N/A", "#N/A"}, // ISBLANK - "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument", + "=ISBLANK(A1,A2)": {"#VALUE!", "ISBLANK requires 1 argument"}, // ISERR - "=ISERR()": "ISERR requires 1 argument", + "=ISERR()": {"#VALUE!", "ISERR requires 1 argument"}, // ISERROR - "=ISERROR()": "ISERROR requires 1 argument", + "=ISERROR()": {"#VALUE!", "ISERROR requires 1 argument"}, // ISEVEN - "=ISEVEN()": "ISEVEN requires 1 argument", - "=ISEVEN(\"text\")": "#VALUE!", - "=ISEVEN(A1:A2)": "#VALUE!", + "=ISEVEN()": {"#VALUE!", "ISEVEN requires 1 argument"}, + "=ISEVEN(\"text\")": {"#VALUE!", "#VALUE!"}, + "=ISEVEN(A1:A2)": {"#VALUE!", "#VALUE!"}, // ISFORMULA - "=ISFORMULA()": "ISFORMULA requires 1 argument", + "=ISFORMULA()": {"#VALUE!", "ISFORMULA requires 1 argument"}, // ISLOGICAL - "=ISLOGICAL()": "ISLOGICAL requires 1 argument", + "=ISLOGICAL()": {"#VALUE!", "ISLOGICAL requires 1 argument"}, // ISNA - "=ISNA()": "ISNA requires 1 argument", + "=ISNA()": {"#VALUE!", "ISNA requires 1 argument"}, // ISNONTEXT - "=ISNONTEXT()": "ISNONTEXT requires 1 argument", + "=ISNONTEXT()": {"#VALUE!", "ISNONTEXT requires 1 argument"}, // ISNUMBER - "=ISNUMBER()": "ISNUMBER requires 1 argument", + "=ISNUMBER()": {"#VALUE!", "ISNUMBER requires 1 argument"}, // ISODD - "=ISODD()": "ISODD requires 1 argument", - "=ISODD(\"text\")": "#VALUE!", + "=ISODD()": {"#VALUE!", "ISODD requires 1 argument"}, + "=ISODD(\"text\")": {"#VALUE!", "#VALUE!"}, // ISREF - "=ISREF()": "ISREF requires 1 argument", + "=ISREF()": {"#VALUE!", "ISREF requires 1 argument"}, // ISTEXT - "=ISTEXT()": "ISTEXT requires 1 argument", + "=ISTEXT()": {"#VALUE!", "ISTEXT requires 1 argument"}, // N - "=N()": "N requires 1 argument", - "=N(NA())": "#N/A", + "=N()": {"#VALUE!", "N requires 1 argument"}, + "=N(NA())": {"#N/A", "#N/A"}, // NA - "=NA()": "#N/A", - "=NA(1)": "NA accepts no arguments", + "=NA()": {"#N/A", "#N/A"}, + "=NA(1)": {"#VALUE!", "NA accepts no arguments"}, // SHEET - "=SHEET(\"\",\"\")": "SHEET accepts at most 1 argument", - "=SHEET(\"Sheet2\")": "#N/A", + "=SHEET(\"\",\"\")": {"#VALUE!", "SHEET accepts at most 1 argument"}, + "=SHEET(\"Sheet2\")": {"#N/A", "#N/A"}, // SHEETS - "=SHEETS(\"\",\"\")": "SHEETS accepts at most 1 argument", - "=SHEETS(\"Sheet1\")": "#N/A", + "=SHEETS(\"\",\"\")": {"#VALUE!", "SHEETS accepts at most 1 argument"}, + "=SHEETS(\"Sheet1\")": {"#N/A", "#N/A"}, // TYPE - "=TYPE()": "TYPE requires 1 argument", + "=TYPE()": {"#VALUE!", "TYPE requires 1 argument"}, // T - "=T()": "T requires 1 argument", - "=T(NA())": "#N/A", + "=T()": {"#VALUE!", "T requires 1 argument"}, + "=T(NA())": {"#N/A", "#N/A"}, // Logical Functions // AND - "=AND(\"text\")": "#VALUE!", - "=AND(A1:B1)": "#VALUE!", - "=AND(\"1\",\"TRUE\",\"FALSE\")": "#VALUE!", - "=AND()": "AND requires at least 1 argument", - "=AND(1" + strings.Repeat(",1", 30) + ")": "AND accepts at most 30 arguments", + "=AND(\"text\")": {"#VALUE!", "#VALUE!"}, + "=AND(A1:B1)": {"#VALUE!", "#VALUE!"}, + "=AND(\"1\",\"TRUE\",\"FALSE\")": {"#VALUE!", "#VALUE!"}, + "=AND()": {"#VALUE!", "AND requires at least 1 argument"}, + "=AND(1" + strings.Repeat(",1", 30) + ")": {"#VALUE!", "AND accepts at most 30 arguments"}, // FALSE - "=FALSE(A1)": "FALSE takes no arguments", + "=FALSE(A1)": {"#VALUE!", "FALSE takes no arguments"}, // IFERROR - "=IFERROR()": "IFERROR requires 2 arguments", + "=IFERROR()": {"#VALUE!", "IFERROR requires 2 arguments"}, // IFNA - "=IFNA()": "IFNA requires 2 arguments", + "=IFNA()": {"#VALUE!", "IFNA requires 2 arguments"}, // IFS - "=IFS()": "IFS requires at least 2 arguments", - "=IFS(FALSE,FALSE)": "#N/A", + "=IFS()": {"#VALUE!", "IFS requires at least 2 arguments"}, + "=IFS(FALSE,FALSE)": {"#N/A", "#N/A"}, // NOT - "=NOT()": "NOT requires 1 argument", - "=NOT(NOT())": "NOT requires 1 argument", - "=NOT(\"\")": "NOT expects 1 boolean or numeric argument", + "=NOT()": {"#VALUE!", "NOT requires 1 argument"}, + "=NOT(NOT())": {"#VALUE!", "NOT requires 1 argument"}, + "=NOT(\"\")": {"#VALUE!", "NOT expects 1 boolean or numeric argument"}, // OR - "=OR(\"text\")": "#VALUE!", - "=OR(A1:B1)": "#VALUE!", - "=OR(\"1\",\"TRUE\",\"FALSE\")": "#VALUE!", - "=OR()": "OR requires at least 1 argument", - "=OR(1" + strings.Repeat(",1", 30) + ")": "OR accepts at most 30 arguments", + "=OR(\"text\")": {"#VALUE!", "#VALUE!"}, + "=OR(A1:B1)": {"#VALUE!", "#VALUE!"}, + "=OR(\"1\",\"TRUE\",\"FALSE\")": {"#VALUE!", "#VALUE!"}, + "=OR()": {"#VALUE!", "OR requires at least 1 argument"}, + "=OR(1" + strings.Repeat(",1", 30) + ")": {"#VALUE!", "OR accepts at most 30 arguments"}, // SWITCH - "=SWITCH()": "SWITCH requires at least 3 arguments", - "=SWITCH(0,1,2)": "#N/A", + "=SWITCH()": {"#VALUE!", "SWITCH requires at least 3 arguments"}, + "=SWITCH(0,1,2)": {"#N/A", "#N/A"}, // TRUE - "=TRUE(A1)": "TRUE takes no arguments", + "=TRUE(A1)": {"#VALUE!", "TRUE takes no arguments"}, // XOR - "=XOR()": "XOR requires at least 1 argument", - "=XOR(\"1\")": "#VALUE!", - "=XOR(\"text\")": "#VALUE!", - "=XOR(XOR(\"text\"))": "#VALUE!", + "=XOR()": {"#VALUE!", "XOR requires at least 1 argument"}, + "=XOR(\"1\")": {"#VALUE!", "#VALUE!"}, + "=XOR(\"text\")": {"#VALUE!", "#VALUE!"}, + "=XOR(XOR(\"text\"))": {"#VALUE!", "#VALUE!"}, // Date and Time Functions // DATE - "=DATE()": "DATE requires 3 number arguments", - `=DATE("text",10,21)`: "DATE requires 3 number arguments", - `=DATE(2020,"text",21)`: "DATE requires 3 number arguments", - `=DATE(2020,10,"text")`: "DATE requires 3 number arguments", + "=DATE()": {"#VALUE!", "DATE requires 3 number arguments"}, + "=DATE(\"text\",10,21)": {"#VALUE!", "DATE requires 3 number arguments"}, + "=DATE(2020,\"text\",21)": {"#VALUE!", "DATE requires 3 number arguments"}, + "=DATE(2020,10,\"text\")": {"#VALUE!", "DATE requires 3 number arguments"}, // DATEDIF - "=DATEDIF()": "DATEDIF requires 3 number arguments", - "=DATEDIF(\"\",\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DATEDIF(43891,43101,\"Y\")": "start_date > end_date", - "=DATEDIF(43101,43891,\"x\")": "DATEDIF has invalid unit", + "=DATEDIF()": {"#VALUE!", "DATEDIF requires 3 number arguments"}, + "=DATEDIF(\"\",\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DATEDIF(43891,43101,\"Y\")": {"#NUM!", "start_date > end_date"}, + "=DATEDIF(43101,43891,\"x\")": {"#VALUE!", "DATEDIF has invalid unit"}, // DATEVALUE - "=DATEVALUE()": "DATEVALUE requires 1 argument", - "=DATEVALUE(\"01/01\")": "#VALUE!", // valid in Excel, which uses years by the system date - "=DATEVALUE(\"1900-0-0\")": "#VALUE!", + "=DATEVALUE()": {"#VALUE!", "DATEVALUE requires 1 argument"}, + "=DATEVALUE(\"01/01\")": {"#VALUE!", "#VALUE!"}, // valid in Excel, which uses years by the system date + "=DATEVALUE(\"1900-0-0\")": {"#VALUE!", "#VALUE!"}, // DAY - "=DAY()": "DAY requires exactly 1 argument", - "=DAY(-1)": "DAY only accepts positive argument", - "=DAY(0,0)": "DAY requires exactly 1 argument", - "=DAY(\"text\")": "#VALUE!", - "=DAY(\"January 25, 2020 9223372036854775808 AM\")": "#VALUE!", - "=DAY(\"January 25, 2020 9223372036854775808:00 AM\")": "#VALUE!", - "=DAY(\"January 25, 2020 00:9223372036854775808 AM\")": "#VALUE!", - "=DAY(\"January 25, 2020 9223372036854775808:00.0 AM\")": "#VALUE!", - "=DAY(\"January 25, 2020 0:1" + strings.Repeat("0", 309) + ".0 AM\")": "#VALUE!", - "=DAY(\"January 25, 2020 9223372036854775808:00:00 AM\")": "#VALUE!", - "=DAY(\"January 25, 2020 0:9223372036854775808:0 AM\")": "#VALUE!", - "=DAY(\"January 25, 2020 0:0:1" + strings.Repeat("0", 309) + " AM\")": "#VALUE!", - "=DAY(\"January 25, 2020 0:61:0 AM\")": "#VALUE!", - "=DAY(\"January 25, 2020 0:00:60 AM\")": "#VALUE!", - "=DAY(\"January 25, 2020 24:00:00\")": "#VALUE!", - "=DAY(\"January 25, 2020 00:00:10001\")": "#VALUE!", - "=DAY(\"9223372036854775808/25/2020\")": "#VALUE!", - "=DAY(\"01/9223372036854775808/2020\")": "#VALUE!", - "=DAY(\"01/25/9223372036854775808\")": "#VALUE!", - "=DAY(\"01/25/10000\")": "#VALUE!", - "=DAY(\"01/25/100\")": "#VALUE!", - "=DAY(\"January 9223372036854775808, 2020\")": "#VALUE!", - "=DAY(\"January 25, 9223372036854775808\")": "#VALUE!", - "=DAY(\"January 25, 10000\")": "#VALUE!", - "=DAY(\"January 25, 100\")": "#VALUE!", - "=DAY(\"9223372036854775808-25-2020\")": "#VALUE!", - "=DAY(\"01-9223372036854775808-2020\")": "#VALUE!", - "=DAY(\"01-25-9223372036854775808\")": "#VALUE!", - "=DAY(\"1900-0-0\")": "#VALUE!", - "=DAY(\"14-25-1900\")": "#VALUE!", - "=DAY(\"3-January-9223372036854775808\")": "#VALUE!", - "=DAY(\"9223372036854775808-January-1900\")": "#VALUE!", - "=DAY(\"0-January-1900\")": "#VALUE!", + "=DAY()": {"#VALUE!", "DAY requires exactly 1 argument"}, + "=DAY(-1)": {"#NUM!", "DAY only accepts positive argument"}, + "=DAY(0,0)": {"#VALUE!", "DAY requires exactly 1 argument"}, + "=DAY(\"text\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 9223372036854775808 AM\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 9223372036854775808:00 AM\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 00:9223372036854775808 AM\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 9223372036854775808:00.0 AM\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 0:1" + strings.Repeat("0", 309) + ".0 AM\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 9223372036854775808:00:00 AM\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 0:9223372036854775808:0 AM\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 0:0:1" + strings.Repeat("0", 309) + " AM\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 0:61:0 AM\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 0:00:60 AM\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 24:00:00\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 2020 00:00:10001\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"9223372036854775808/25/2020\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"01/9223372036854775808/2020\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"01/25/9223372036854775808\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"01/25/10000\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"01/25/100\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 9223372036854775808, 2020\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 9223372036854775808\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 10000\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"January 25, 100\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"9223372036854775808-25-2020\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"01-9223372036854775808-2020\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"01-25-9223372036854775808\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"1900-0-0\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"14-25-1900\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"3-January-9223372036854775808\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"9223372036854775808-January-1900\")": {"#VALUE!", "#VALUE!"}, + "=DAY(\"0-January-1900\")": {"#VALUE!", "#VALUE!"}, // DAYS - "=DAYS()": "DAYS requires 2 arguments", - "=DAYS(\"\",0)": "#VALUE!", - "=DAYS(0,\"\")": "#VALUE!", - "=DAYS(NA(),0)": "#VALUE!", - "=DAYS(0,NA())": "#VALUE!", + "=DAYS()": {"#VALUE!", "DAYS requires 2 arguments"}, + "=DAYS(\"\",0)": {"#VALUE!", "#VALUE!"}, + "=DAYS(0,\"\")": {"#VALUE!", "#VALUE!"}, + "=DAYS(NA(),0)": {"#VALUE!", "#VALUE!"}, + "=DAYS(0,NA())": {"#VALUE!", "#VALUE!"}, // DAYS360 - "=DAYS360(\"12/12/1999\")": "DAYS360 requires at least 2 arguments", - "=DAYS360(\"12/12/1999\", \"11/30/1999\",TRUE,\"\")": "DAYS360 requires at most 3 arguments", - "=DAYS360(\"12/12/1999\", \"11/30/1999\",\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=DAYS360(\"12/12/1999\", \"\")": "#VALUE!", - "=DAYS360(\"\", \"11/30/1999\")": "#VALUE!", + "=DAYS360(\"12/12/1999\")": {"#VALUE!", "DAYS360 requires at least 2 arguments"}, + "=DAYS360(\"12/12/1999\", \"11/30/1999\",TRUE,\"\")": {"#VALUE!", "DAYS360 requires at most 3 arguments"}, + "=DAYS360(\"12/12/1999\", \"11/30/1999\",\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=DAYS360(\"12/12/1999\", \"\")": {"#VALUE!", "#VALUE!"}, + "=DAYS360(\"\", \"11/30/1999\")": {"#VALUE!", "#VALUE!"}, // EDATE - "=EDATE()": "EDATE requires 2 arguments", - "=EDATE(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=EDATE(-1,0)": "#NUM!", - "=EDATE(\"\",0)": "#VALUE!", - "=EDATE(\"January 25, 100\",0)": "#VALUE!", + "=EDATE()": {"#VALUE!", "EDATE requires 2 arguments"}, + "=EDATE(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=EDATE(-1,0)": {"#NUM!", "#NUM!"}, + "=EDATE(\"\",0)": {"#VALUE!", "#VALUE!"}, + "=EDATE(\"January 25, 100\",0)": {"#VALUE!", "#VALUE!"}, // EOMONTH - "=EOMONTH()": "EOMONTH requires 2 arguments", - "=EOMONTH(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=EOMONTH(-1,0)": "#NUM!", - "=EOMONTH(\"\",0)": "#VALUE!", - "=EOMONTH(\"January 25, 100\",0)": "#VALUE!", + "=EOMONTH()": {"#VALUE!", "EOMONTH requires 2 arguments"}, + "=EOMONTH(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=EOMONTH(-1,0)": {"#NUM!", "#NUM!"}, + "=EOMONTH(\"\",0)": {"#VALUE!", "#VALUE!"}, + "=EOMONTH(\"January 25, 100\",0)": {"#VALUE!", "#VALUE!"}, // HOUR - "=HOUR()": "HOUR requires exactly 1 argument", - "=HOUR(-1)": "HOUR only accepts positive argument", - "=HOUR(\"\")": "#VALUE!", - "=HOUR(\"25:10:55\")": "#VALUE!", + "=HOUR()": {"#VALUE!", "HOUR requires exactly 1 argument"}, + "=HOUR(-1)": {"#NUM!", "HOUR only accepts positive argument"}, + "=HOUR(\"\")": {"#VALUE!", "#VALUE!"}, + "=HOUR(\"25:10:55\")": {"#VALUE!", "#VALUE!"}, // ISOWEEKNUM - "=ISOWEEKNUM()": "ISOWEEKNUM requires 1 argument", - "=ISOWEEKNUM(\"\")": "#VALUE!", - "=ISOWEEKNUM(\"January 25, 100\")": "#VALUE!", - "=ISOWEEKNUM(-1)": "#NUM!", + "=ISOWEEKNUM()": {"#VALUE!", "ISOWEEKNUM requires 1 argument"}, + "=ISOWEEKNUM(\"\")": {"#VALUE!", "#VALUE!"}, + "=ISOWEEKNUM(\"January 25, 100\")": {"#VALUE!", "#VALUE!"}, + "=ISOWEEKNUM(-1)": {"#NUM!", "#NUM!"}, // MINUTE - "=MINUTE()": "MINUTE requires exactly 1 argument", - "=MINUTE(-1)": "MINUTE only accepts positive argument", - "=MINUTE(\"\")": "#VALUE!", - "=MINUTE(\"13:60:55\")": "#VALUE!", + "=MINUTE()": {"#VALUE!", "MINUTE requires exactly 1 argument"}, + "=MINUTE(-1)": {"#NUM!", "MINUTE only accepts positive argument"}, + "=MINUTE(\"\")": {"#VALUE!", "#VALUE!"}, + "=MINUTE(\"13:60:55\")": {"#VALUE!", "#VALUE!"}, // MONTH - "=MONTH()": "MONTH requires exactly 1 argument", - "=MONTH(0,0)": "MONTH requires exactly 1 argument", - "=MONTH(-1)": "MONTH only accepts positive argument", - "=MONTH(\"text\")": "#VALUE!", - "=MONTH(\"January 25, 100\")": "#VALUE!", + "=MONTH()": {"#VALUE!", "MONTH requires exactly 1 argument"}, + "=MONTH(0,0)": {"#VALUE!", "MONTH requires exactly 1 argument"}, + "=MONTH(-1)": {"#NUM!", "MONTH only accepts positive argument"}, + "=MONTH(\"text\")": {"#VALUE!", "#VALUE!"}, + "=MONTH(\"January 25, 100\")": {"#VALUE!", "#VALUE!"}, // YEAR - "=YEAR()": "YEAR requires exactly 1 argument", - "=YEAR(0,0)": "YEAR requires exactly 1 argument", - "=YEAR(-1)": "YEAR only accepts positive argument", - "=YEAR(\"text\")": "#VALUE!", - "=YEAR(\"January 25, 100\")": "#VALUE!", + "=YEAR()": {"#VALUE!", "YEAR requires exactly 1 argument"}, + "=YEAR(0,0)": {"#VALUE!", "YEAR requires exactly 1 argument"}, + "=YEAR(-1)": {"#NUM!", "YEAR only accepts positive argument"}, + "=YEAR(\"text\")": {"#VALUE!", "#VALUE!"}, + "=YEAR(\"January 25, 100\")": {"#VALUE!", "#VALUE!"}, // YEARFRAC - "=YEARFRAC()": "YEARFRAC requires 3 or 4 arguments", - "=YEARFRAC(42005,42094,5)": "invalid basis", - "=YEARFRAC(\"\",42094,5)": "#VALUE!", - "=YEARFRAC(42005,\"\",5)": "#VALUE!", - "=YEARFRAC(42005,42094,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=YEARFRAC()": {"#VALUE!", "YEARFRAC requires 3 or 4 arguments"}, + "=YEARFRAC(42005,42094,5)": {"#NUM!", "invalid basis"}, + "=YEARFRAC(\"\",42094,5)": {"#VALUE!", "#VALUE!"}, + "=YEARFRAC(42005,\"\",5)": {"#VALUE!", "#VALUE!"}, + "=YEARFRAC(42005,42094,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // NOW - "=NOW(A1)": "NOW accepts no arguments", + "=NOW(A1)": {"#VALUE!", "NOW accepts no arguments"}, // SECOND - "=SECOND()": "SECOND requires exactly 1 argument", - "=SECOND(-1)": "SECOND only accepts positive argument", - "=SECOND(\"\")": "#VALUE!", - "=SECOND(\"25:55\")": "#VALUE!", + "=SECOND()": {"#VALUE!", "SECOND requires exactly 1 argument"}, + "=SECOND(-1)": {"#NUM!", "SECOND only accepts positive argument"}, + "=SECOND(\"\")": {"#VALUE!", "#VALUE!"}, + "=SECOND(\"25:55\")": {"#VALUE!", "#VALUE!"}, // TIME - "=TIME()": "TIME requires 3 number arguments", - "=TIME(\"\",0,0)": "TIME requires 3 number arguments", - "=TIME(0,0,-1)": "#NUM!", + "=TIME()": {"#VALUE!", "TIME requires 3 number arguments"}, + "=TIME(\"\",0,0)": {"#VALUE!", "TIME requires 3 number arguments"}, + "=TIME(0,0,-1)": {"#NUM!", "#NUM!"}, // TIMEVALUE - "=TIMEVALUE()": "TIMEVALUE requires exactly 1 argument", - "=TIMEVALUE(1)": "#VALUE!", - "=TIMEVALUE(-1)": "#VALUE!", - "=TIMEVALUE(\"25:55\")": "#VALUE!", + "=TIMEVALUE()": {"#VALUE!", "TIMEVALUE requires exactly 1 argument"}, + "=TIMEVALUE(1)": {"#VALUE!", "#VALUE!"}, + "=TIMEVALUE(-1)": {"#VALUE!", "#VALUE!"}, + "=TIMEVALUE(\"25:55\")": {"#VALUE!", "#VALUE!"}, // TODAY - "=TODAY(A1)": "TODAY accepts no arguments", + "=TODAY(A1)": {"#VALUE!", "TODAY accepts no arguments"}, // WEEKDAY - "=WEEKDAY()": "WEEKDAY requires at least 1 argument", - "=WEEKDAY(0,1,0)": "WEEKDAY allows at most 2 arguments", - "=WEEKDAY(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=WEEKDAY(\"\",1)": "#VALUE!", - "=WEEKDAY(0,0)": "#VALUE!", - "=WEEKDAY(\"January 25, 100\")": "#VALUE!", - "=WEEKDAY(-1,1)": "#NUM!", + "=WEEKDAY()": {"#VALUE!", "WEEKDAY requires at least 1 argument"}, + "=WEEKDAY(0,1,0)": {"#VALUE!", "WEEKDAY allows at most 2 arguments"}, + "=WEEKDAY(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=WEEKDAY(\"\",1)": {"#VALUE!", "#VALUE!"}, + "=WEEKDAY(0,0)": {"#VALUE!", "#VALUE!"}, + "=WEEKDAY(\"January 25, 100\")": {"#VALUE!", "#VALUE!"}, + "=WEEKDAY(-1,1)": {"#NUM!", "#NUM!"}, // WEEKNUM - "=WEEKNUM()": "WEEKNUM requires at least 1 argument", - "=WEEKNUM(0,1,0)": "WEEKNUM allows at most 2 arguments", - "=WEEKNUM(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=WEEKNUM(\"\",1)": "#VALUE!", - "=WEEKNUM(\"January 25, 100\")": "#VALUE!", - "=WEEKNUM(0,0)": "#NUM!", - "=WEEKNUM(-1,1)": "#NUM!", + "=WEEKNUM()": {"#VALUE!", "WEEKNUM requires at least 1 argument"}, + "=WEEKNUM(0,1,0)": {"#VALUE!", "WEEKNUM allows at most 2 arguments"}, + "=WEEKNUM(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=WEEKNUM(\"\",1)": {"#VALUE!", "#VALUE!"}, + "=WEEKNUM(\"January 25, 100\")": {"#VALUE!", "#VALUE!"}, + "=WEEKNUM(0,0)": {"#NUM!", "#NUM!"}, + "=WEEKNUM(-1,1)": {"#NUM!", "#NUM!"}, // Text Functions // CHAR - "=CHAR()": "CHAR requires 1 argument", - "=CHAR(-1)": "#VALUE!", - "=CHAR(256)": "#VALUE!", - "=CHAR(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHAR()": {"#VALUE!", "CHAR requires 1 argument"}, + "=CHAR(-1)": {"#VALUE!", "#VALUE!"}, + "=CHAR(256)": {"#VALUE!", "#VALUE!"}, + "=CHAR(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // CLEAN - "=CLEAN()": "CLEAN requires 1 argument", - "=CLEAN(1,2)": "CLEAN requires 1 argument", + "=CLEAN()": {"#VALUE!", "CLEAN requires 1 argument"}, + "=CLEAN(1,2)": {"#VALUE!", "CLEAN requires 1 argument"}, // CODE - "=CODE()": "CODE requires 1 argument", - "=CODE(1,2)": "CODE requires 1 argument", + "=CODE()": {"#VALUE!", "CODE requires 1 argument"}, + "=CODE(1,2)": {"#VALUE!", "CODE requires 1 argument"}, // CONCAT - "=CONCAT(MUNIT(2))": "CONCAT requires arguments to be strings", + "=CONCAT(MUNIT(2))": {"#VALUE!", "CONCAT requires arguments to be strings"}, // CONCATENATE - "=CONCATENATE(MUNIT(2))": "CONCATENATE requires arguments to be strings", + "=CONCATENATE(MUNIT(2))": {"#VALUE!", "CONCATENATE requires arguments to be strings"}, // EXACT - "=EXACT()": "EXACT requires 2 arguments", - "=EXACT(1,2,3)": "EXACT requires 2 arguments", + "=EXACT()": {"#VALUE!", "EXACT requires 2 arguments"}, + "=EXACT(1,2,3)": {"#VALUE!", "EXACT requires 2 arguments"}, // FIXED - "=FIXED()": "FIXED requires at least 1 argument", - "=FIXED(0,1,2,3)": "FIXED allows at most 3 arguments", - "=FIXED(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FIXED(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FIXED(0,0,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", + "=FIXED()": {"#VALUE!", "FIXED requires at least 1 argument"}, + "=FIXED(0,1,2,3)": {"#VALUE!", "FIXED allows at most 3 arguments"}, + "=FIXED(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FIXED(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FIXED(0,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, // FIND - "=FIND()": "FIND requires at least 2 arguments", - "=FIND(1,2,3,4)": "FIND allows at most 3 arguments", - "=FIND(\"x\",\"\")": "#VALUE!", - "=FIND(\"x\",\"x\",-1)": "#VALUE!", - "=FIND(\"x\",\"x\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=FIND()": {"#VALUE!", "FIND requires at least 2 arguments"}, + "=FIND(1,2,3,4)": {"#VALUE!", "FIND allows at most 3 arguments"}, + "=FIND(\"x\",\"\")": {"#VALUE!", "#VALUE!"}, + "=FIND(\"x\",\"x\",-1)": {"#VALUE!", "#VALUE!"}, + "=FIND(\"x\",\"x\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // FINDB - "=FINDB()": "FINDB requires at least 2 arguments", - "=FINDB(1,2,3,4)": "FINDB allows at most 3 arguments", - "=FINDB(\"x\",\"\")": "#VALUE!", - "=FINDB(\"x\",\"x\",-1)": "#VALUE!", - "=FINDB(\"x\",\"x\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=FINDB()": {"#VALUE!", "FINDB requires at least 2 arguments"}, + "=FINDB(1,2,3,4)": {"#VALUE!", "FINDB allows at most 3 arguments"}, + "=FINDB(\"x\",\"\")": {"#VALUE!", "#VALUE!"}, + "=FINDB(\"x\",\"x\",-1)": {"#VALUE!", "#VALUE!"}, + "=FINDB(\"x\",\"x\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // LEFT - "=LEFT()": "LEFT requires at least 1 argument", - "=LEFT(\"\",2,3)": "LEFT allows at most 2 arguments", - "=LEFT(\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LEFT(\"\",-1)": "#VALUE!", + "=LEFT()": {"#VALUE!", "LEFT requires at least 1 argument"}, + "=LEFT(\"\",2,3)": {"#VALUE!", "LEFT allows at most 2 arguments"}, + "=LEFT(\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LEFT(\"\",-1)": {"#VALUE!", "#VALUE!"}, // LEFTB - "=LEFTB()": "LEFTB requires at least 1 argument", - "=LEFTB(\"\",2,3)": "LEFTB allows at most 2 arguments", - "=LEFTB(\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=LEFTB(\"\",-1)": "#VALUE!", + "=LEFTB()": {"#VALUE!", "LEFTB requires at least 1 argument"}, + "=LEFTB(\"\",2,3)": {"#VALUE!", "LEFTB allows at most 2 arguments"}, + "=LEFTB(\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=LEFTB(\"\",-1)": {"#VALUE!", "#VALUE!"}, // LEN - "=LEN()": "LEN requires 1 string argument", + "=LEN()": {"#VALUE!", "LEN requires 1 string argument"}, // LENB - "=LENB()": "LENB requires 1 string argument", + "=LENB()": {"#VALUE!", "LENB requires 1 string argument"}, // LOWER - "=LOWER()": "LOWER requires 1 argument", - "=LOWER(1,2)": "LOWER requires 1 argument", + "=LOWER()": {"#VALUE!", "LOWER requires 1 argument"}, + "=LOWER(1,2)": {"#VALUE!", "LOWER requires 1 argument"}, // MID - "=MID()": "MID requires 3 arguments", - "=MID(\"\",-1,1)": "#VALUE!", - "=MID(\"\",\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MID(\"\",1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=MID()": {"#VALUE!", "MID requires 3 arguments"}, + "=MID(\"\",-1,1)": {"#VALUE!", "#VALUE!"}, + "=MID(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=MID(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // MIDB - "=MIDB()": "MIDB requires 3 arguments", - "=MIDB(\"\",-1,1)": "#VALUE!", - "=MIDB(\"\",\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MIDB(\"\",1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=MIDB()": {"#VALUE!", "MIDB requires 3 arguments"}, + "=MIDB(\"\",-1,1)": {"#VALUE!", "#VALUE!"}, + "=MIDB(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=MIDB(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // PROPER - "=PROPER()": "PROPER requires 1 argument", - "=PROPER(1,2)": "PROPER requires 1 argument", + "=PROPER()": {"#VALUE!", "PROPER requires 1 argument"}, + "=PROPER(1,2)": {"#VALUE!", "PROPER requires 1 argument"}, // REPLACE - "=REPLACE()": "REPLACE requires 4 arguments", - "=REPLACE(\"text\",0,4,\"string\")": "#VALUE!", - "=REPLACE(\"text\",\"\",0,\"string\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=REPLACE(\"text\",1,\"\",\"string\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=REPLACE()": {"#VALUE!", "REPLACE requires 4 arguments"}, + "=REPLACE(\"text\",0,4,\"string\")": {"#VALUE!", "#VALUE!"}, + "=REPLACE(\"text\",\"\",0,\"string\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=REPLACE(\"text\",1,\"\",\"string\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // REPLACEB - "=REPLACEB()": "REPLACEB requires 4 arguments", - "=REPLACEB(\"text\",0,4,\"string\")": "#VALUE!", - "=REPLACEB(\"text\",\"\",0,\"string\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=REPLACEB(\"text\",1,\"\",\"string\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=REPLACEB()": {"#VALUE!", "REPLACEB requires 4 arguments"}, + "=REPLACEB(\"text\",0,4,\"string\")": {"#VALUE!", "#VALUE!"}, + "=REPLACEB(\"text\",\"\",0,\"string\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=REPLACEB(\"text\",1,\"\",\"string\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // REPT - "=REPT()": "REPT requires 2 arguments", - "=REPT(INT(0),2)": "REPT requires first argument to be a string", - "=REPT(\"*\",\"*\")": "REPT requires second argument to be a number", - "=REPT(\"*\",-1)": "REPT requires second argument to be >= 0", + "=REPT()": {"#VALUE!", "REPT requires 2 arguments"}, + "=REPT(INT(0),2)": {"#VALUE!", "REPT requires first argument to be a string"}, + "=REPT(\"*\",\"*\")": {"#VALUE!", "REPT requires second argument to be a number"}, + "=REPT(\"*\",-1)": {"#VALUE!", "REPT requires second argument to be >= 0"}, // RIGHT - "=RIGHT()": "RIGHT requires at least 1 argument", - "=RIGHT(\"\",2,3)": "RIGHT allows at most 2 arguments", - "=RIGHT(\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=RIGHT(\"\",-1)": "#VALUE!", + "=RIGHT()": {"#VALUE!", "RIGHT requires at least 1 argument"}, + "=RIGHT(\"\",2,3)": {"#VALUE!", "RIGHT allows at most 2 arguments"}, + "=RIGHT(\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=RIGHT(\"\",-1)": {"#VALUE!", "#VALUE!"}, // RIGHTB - "=RIGHTB()": "RIGHTB requires at least 1 argument", - "=RIGHTB(\"\",2,3)": "RIGHTB allows at most 2 arguments", - "=RIGHTB(\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=RIGHTB(\"\",-1)": "#VALUE!", + "=RIGHTB()": {"#VALUE!", "RIGHTB requires at least 1 argument"}, + "=RIGHTB(\"\",2,3)": {"#VALUE!", "RIGHTB allows at most 2 arguments"}, + "=RIGHTB(\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=RIGHTB(\"\",-1)": {"#VALUE!", "#VALUE!"}, // SUBSTITUTE - "=SUBSTITUTE()": "SUBSTITUTE requires 3 or 4 arguments", - "=SUBSTITUTE(\"\",\"\",\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=SUBSTITUTE(\"\",\"\",\"\",0)": "instance_num should be > 0", + "=SUBSTITUTE()": {"#VALUE!", "SUBSTITUTE requires 3 or 4 arguments"}, + "=SUBSTITUTE(\"\",\"\",\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=SUBSTITUTE(\"\",\"\",\"\",0)": {"#VALUE!", "instance_num should be > 0"}, // TEXTJOIN - "=TEXTJOIN()": "TEXTJOIN requires at least 3 arguments", - "=TEXTJOIN(\"\",\"\",1)": "#VALUE!", - "=TEXTJOIN(\"\",TRUE,NA())": "#N/A", - "=TEXTJOIN(\"\",TRUE," + strings.Repeat("0,", 250) + ",0)": "TEXTJOIN accepts at most 252 arguments", - "=TEXTJOIN(\",\",FALSE,REPT(\"*\",32768))": "TEXTJOIN function exceeds 32767 characters", + "=TEXTJOIN()": {"#VALUE!", "TEXTJOIN requires at least 3 arguments"}, + "=TEXTJOIN(\"\",\"\",1)": {"#VALUE!", "#VALUE!"}, + "=TEXTJOIN(\"\",TRUE,NA())": {"#N/A", "#N/A"}, + "=TEXTJOIN(\"\",TRUE," + strings.Repeat("0,", 250) + ",0)": {"#VALUE!", "TEXTJOIN accepts at most 252 arguments"}, + "=TEXTJOIN(\",\",FALSE,REPT(\"*\",32768))": {"#VALUE!", "TEXTJOIN function exceeds 32767 characters"}, // TRIM - "=TRIM()": "TRIM requires 1 argument", - "=TRIM(1,2)": "TRIM requires 1 argument", + "=TRIM()": {"#VALUE!", "TRIM requires 1 argument"}, + "=TRIM(1,2)": {"#VALUE!", "TRIM requires 1 argument"}, // UNICHAR - "=UNICHAR()": "UNICHAR requires 1 argument", - "=UNICHAR(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=UNICHAR(55296)": "#VALUE!", - "=UNICHAR(0)": "#VALUE!", + "=UNICHAR()": {"#VALUE!", "UNICHAR requires 1 argument"}, + "=UNICHAR(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=UNICHAR(55296)": {"#VALUE!", "#VALUE!"}, + "=UNICHAR(0)": {"#VALUE!", "#VALUE!"}, // UNICODE - "=UNICODE()": "UNICODE requires 1 argument", - "=UNICODE(\"\")": "#VALUE!", + "=UNICODE()": {"#VALUE!", "UNICODE requires 1 argument"}, + "=UNICODE(\"\")": {"#VALUE!", "#VALUE!"}, // VALUE - "=VALUE()": "VALUE requires 1 argument", - "=VALUE(\"\")": "#VALUE!", + "=VALUE()": {"#VALUE!", "VALUE requires 1 argument"}, + "=VALUE(\"\")": {"#VALUE!", "#VALUE!"}, // UPPER - "=UPPER()": "UPPER requires 1 argument", - "=UPPER(1,2)": "UPPER requires 1 argument", + "=UPPER()": {"#VALUE!", "UPPER requires 1 argument"}, + "=UPPER(1,2)": {"#VALUE!", "UPPER requires 1 argument"}, // Conditional Functions // IF - "=IF()": "IF requires at least 1 argument", - "=IF(0,1,2,3)": "IF accepts at most 3 arguments", - "=IF(D1,1,2)": "strconv.ParseBool: parsing \"Month\": invalid syntax", + "=IF()": {"#VALUE!", "IF requires at least 1 argument"}, + "=IF(0,1,2,3)": {"#VALUE!", "IF accepts at most 3 arguments"}, + "=IF(D1,1,2)": {"#VALUE!", "strconv.ParseBool: parsing \"Month\": invalid syntax"}, // Excel Lookup and Reference Functions // ADDRESS - "=ADDRESS()": "ADDRESS requires at least 2 arguments", - "=ADDRESS(1,1,1,TRUE,\"Sheet1\",0)": "ADDRESS requires at most 5 arguments", - "=ADDRESS(\"\",1,1,TRUE)": "#VALUE!", - "=ADDRESS(1,\"\",1,TRUE)": "#VALUE!", - "=ADDRESS(1,1,\"\",TRUE)": "#VALUE!", - "=ADDRESS(1,1,1,\"\")": "#VALUE!", - "=ADDRESS(1,1,0,TRUE)": "#NUM!", - "=ADDRESS(1,16385,2,TRUE)": "#VALUE!", - "=ADDRESS(1,16385,3,TRUE)": "#VALUE!", - "=ADDRESS(1048576,1,1,TRUE)": "#VALUE!", + "=ADDRESS()": {"#VALUE!", "ADDRESS requires at least 2 arguments"}, + "=ADDRESS(1,1,1,TRUE,\"Sheet1\",0)": {"#VALUE!", "ADDRESS requires at most 5 arguments"}, + "=ADDRESS(\"\",1,1,TRUE)": {"#VALUE!", "#VALUE!"}, + "=ADDRESS(1,\"\",1,TRUE)": {"#VALUE!", "#VALUE!"}, + "=ADDRESS(1,1,\"\",TRUE)": {"#VALUE!", "#VALUE!"}, + "=ADDRESS(1,1,1,\"\")": {"#VALUE!", "#VALUE!"}, + "=ADDRESS(1,1,0,TRUE)": {"#NUM!", "#NUM!"}, + "=ADDRESS(1,16385,2,TRUE)": {"#VALUE!", "#VALUE!"}, + "=ADDRESS(1,16385,3,TRUE)": {"#VALUE!", "#VALUE!"}, + "=ADDRESS(1048576,1,1,TRUE)": {"#VALUE!", "#VALUE!"}, // CHOOSE - "=CHOOSE()": "CHOOSE requires 2 arguments", - "=CHOOSE(\"index_num\",0)": "CHOOSE requires first argument of type number", - "=CHOOSE(2,0)": "index_num should be <= to the number of values", - "=CHOOSE(1,NA())": "#N/A", + "=CHOOSE()": {"#VALUE!", "CHOOSE requires 2 arguments"}, + "=CHOOSE(\"index_num\",0)": {"#VALUE!", "CHOOSE requires first argument of type number"}, + "=CHOOSE(2,0)": {"#VALUE!", "index_num should be <= to the number of values"}, + "=CHOOSE(1,NA())": {"#N/A", "#N/A"}, // COLUMN - "=COLUMN(1,2)": "COLUMN requires at most 1 argument", - "=COLUMN(\"\")": "invalid reference", - "=COLUMN(Sheet1)": newInvalidColumnNameError("Sheet1").Error(), - "=COLUMN(Sheet1!A1!B1)": newInvalidColumnNameError("Sheet1").Error(), + "=COLUMN(1,2)": {"#VALUE!", "COLUMN requires at most 1 argument"}, + "=COLUMN(\"\")": {"#VALUE!", "invalid reference"}, + "=COLUMN(Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, + "=COLUMN(Sheet1!A1!B1)": {"", newInvalidColumnNameError("Sheet1").Error()}, // COLUMNS - "=COLUMNS()": "COLUMNS requires 1 argument", - "=COLUMNS(1)": "invalid reference", - "=COLUMNS(\"\")": "invalid reference", - "=COLUMNS(Sheet1)": newInvalidColumnNameError("Sheet1").Error(), - "=COLUMNS(Sheet1!A1!B1)": newInvalidColumnNameError("Sheet1").Error(), - "=COLUMNS(Sheet1!Sheet1)": newInvalidColumnNameError("Sheet1").Error(), + "=COLUMNS()": {"#VALUE!", "COLUMNS requires 1 argument"}, + "=COLUMNS(1)": {"#VALUE!", "invalid reference"}, + "=COLUMNS(\"\")": {"#VALUE!", "invalid reference"}, + "=COLUMNS(Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, + "=COLUMNS(Sheet1!A1!B1)": {"", newInvalidColumnNameError("Sheet1").Error()}, + "=COLUMNS(Sheet1!Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, // FORMULATEXT - "=FORMULATEXT()": "FORMULATEXT requires 1 argument", - "=FORMULATEXT(1)": "#VALUE!", + "=FORMULATEXT()": {"#VALUE!", "FORMULATEXT requires 1 argument"}, + "=FORMULATEXT(1)": {"#VALUE!", "#VALUE!"}, // HLOOKUP - "=HLOOKUP()": "HLOOKUP requires at least 3 arguments", - "=HLOOKUP(D2,D1,1,FALSE)": "HLOOKUP requires second argument of table array", - "=HLOOKUP(D2,D:D,FALSE,FALSE)": "HLOOKUP requires numeric row argument", - "=HLOOKUP(D2,D:D,1,FALSE,FALSE)": "HLOOKUP requires at most 4 arguments", - "=HLOOKUP(D2,D:D,1,2)": "HLOOKUP no result found", - "=HLOOKUP(D2,D10:D10,1,FALSE)": "HLOOKUP no result found", - "=HLOOKUP(D2,D2:D3,4,FALSE)": "HLOOKUP has invalid row index", - "=HLOOKUP(D2,C:C,1,FALSE)": "HLOOKUP no result found", - "=HLOOKUP(ISNUMBER(1),F3:F9,1)": "HLOOKUP no result found", - "=HLOOKUP(INT(1),E2:E9,1)": "HLOOKUP no result found", - "=HLOOKUP(MUNIT(2),MUNIT(3),1)": "HLOOKUP no result found", - "=HLOOKUP(A1:B2,B2:B3,1)": "HLOOKUP no result found", + "=HLOOKUP()": {"#VALUE!", "HLOOKUP requires at least 3 arguments"}, + "=HLOOKUP(D2,D1,1,FALSE)": {"#VALUE!", "HLOOKUP requires second argument of table array"}, + "=HLOOKUP(D2,D:D,FALSE,FALSE)": {"#VALUE!", "HLOOKUP requires numeric row argument"}, + "=HLOOKUP(D2,D:D,1,FALSE,FALSE)": {"#VALUE!", "HLOOKUP requires at most 4 arguments"}, + "=HLOOKUP(D2,D:D,1,2)": {"#N/A", "HLOOKUP no result found"}, + "=HLOOKUP(D2,D10:D10,1,FALSE)": {"#N/A", "HLOOKUP no result found"}, + "=HLOOKUP(D2,D2:D3,4,FALSE)": {"#N/A", "HLOOKUP has invalid row index"}, + "=HLOOKUP(D2,C:C,1,FALSE)": {"#N/A", "HLOOKUP no result found"}, + "=HLOOKUP(ISNUMBER(1),F3:F9,1)": {"#N/A", "HLOOKUP no result found"}, + "=HLOOKUP(INT(1),E2:E9,1)": {"#N/A", "HLOOKUP no result found"}, + "=HLOOKUP(MUNIT(2),MUNIT(3),1)": {"#N/A", "HLOOKUP no result found"}, + "=HLOOKUP(A1:B2,B2:B3,1)": {"#N/A", "HLOOKUP no result found"}, // MATCH - "=MATCH()": "MATCH requires 1 or 2 arguments", - "=MATCH(0,A1:A1,0,0)": "MATCH requires 1 or 2 arguments", - "=MATCH(0,A1:A1,\"x\")": "MATCH requires numeric match_type argument", - "=MATCH(0,A1)": "MATCH arguments lookup_array should be one-dimensional array", - "=MATCH(0,A1:B1)": "MATCH arguments lookup_array should be one-dimensional array", + "=MATCH()": {"#VALUE!", "MATCH requires 1 or 2 arguments"}, + "=MATCH(0,A1:A1,0,0)": {"#VALUE!", "MATCH requires 1 or 2 arguments"}, + "=MATCH(0,A1:A1,\"x\")": {"#VALUE!", "MATCH requires numeric match_type argument"}, + "=MATCH(0,A1)": {"#N/A", "MATCH arguments lookup_array should be one-dimensional array"}, + "=MATCH(0,A1:B1)": {"#N/A", "MATCH arguments lookup_array should be one-dimensional array"}, // TRANSPOSE - "=TRANSPOSE()": "TRANSPOSE requires 1 argument", + "=TRANSPOSE()": {"#VALUE!", "TRANSPOSE requires 1 argument"}, // HYPERLINK - "=HYPERLINK()": "HYPERLINK requires at least 1 argument", - "=HYPERLINK(\"https://github.com/xuri/excelize\",\"Excelize\",\"\")": "HYPERLINK allows at most 2 arguments", + "=HYPERLINK()": {"#VALUE!", "HYPERLINK requires at least 1 argument"}, + "=HYPERLINK(\"https://github.com/xuri/excelize\",\"Excelize\",\"\")": {"#VALUE!", "HYPERLINK allows at most 2 arguments"}, // VLOOKUP - "=VLOOKUP()": "VLOOKUP requires at least 3 arguments", - "=VLOOKUP(D2,D1,1,FALSE)": "VLOOKUP requires second argument of table array", - "=VLOOKUP(D2,D:D,FALSE,FALSE)": "VLOOKUP requires numeric col argument", - "=VLOOKUP(D2,D:D,1,FALSE,FALSE)": "VLOOKUP requires at most 4 arguments", - "=VLOOKUP(D2,D10:D10,1,FALSE)": "VLOOKUP no result found", - "=VLOOKUP(D2,D:D,2,FALSE)": "VLOOKUP has invalid column index", - "=VLOOKUP(D2,C:C,1,FALSE)": "VLOOKUP no result found", - "=VLOOKUP(ISNUMBER(1),F3:F9,1)": "VLOOKUP no result found", - "=VLOOKUP(INT(1),E2:E9,1)": "VLOOKUP no result found", - "=VLOOKUP(MUNIT(2),MUNIT(3),1)": "VLOOKUP no result found", - "=VLOOKUP(1,G1:H2,1,FALSE)": "VLOOKUP no result found", + "=VLOOKUP()": {"#VALUE!", "VLOOKUP requires at least 3 arguments"}, + "=VLOOKUP(D2,D1,1,FALSE)": {"#VALUE!", "VLOOKUP requires second argument of table array"}, + "=VLOOKUP(D2,D:D,FALSE,FALSE)": {"#VALUE!", "VLOOKUP requires numeric col argument"}, + "=VLOOKUP(D2,D:D,1,FALSE,FALSE)": {"#VALUE!", "VLOOKUP requires at most 4 arguments"}, + "=VLOOKUP(D2,D10:D10,1,FALSE)": {"#N/A", "VLOOKUP no result found"}, + "=VLOOKUP(D2,D:D,2,FALSE)": {"#N/A", "VLOOKUP has invalid column index"}, + "=VLOOKUP(D2,C:C,1,FALSE)": {"#N/A", "VLOOKUP no result found"}, + "=VLOOKUP(ISNUMBER(1),F3:F9,1)": {"#N/A", "VLOOKUP no result found"}, + "=VLOOKUP(INT(1),E2:E9,1)": {"#N/A", "VLOOKUP no result found"}, + "=VLOOKUP(MUNIT(2),MUNIT(3),1)": {"#N/A", "VLOOKUP no result found"}, + "=VLOOKUP(1,G1:H2,1,FALSE)": {"#N/A", "VLOOKUP no result found"}, // INDEX - "=INDEX()": "INDEX requires 2 or 3 arguments", - "=INDEX(A1,2)": "INDEX row_num out of range", - "=INDEX(A1,0,2)": "INDEX col_num out of range", - "=INDEX(A1:A1,2)": "INDEX row_num out of range", - "=INDEX(A1:A1,0,2)": "INDEX col_num out of range", - "=INDEX(A1:B2,2,3)": "INDEX col_num out of range", - "=INDEX(A1:A2,0,0)": "#VALUE!", - "=INDEX(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=INDEX(0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=INDEX()": {"#VALUE!", "INDEX requires 2 or 3 arguments"}, + "=INDEX(A1,2)": {"#REF!", "INDEX row_num out of range"}, + "=INDEX(A1,0,2)": {"#REF!", "INDEX col_num out of range"}, + "=INDEX(A1:A1,2)": {"#REF!", "INDEX row_num out of range"}, + "=INDEX(A1:A1,0,2)": {"#REF!", "INDEX col_num out of range"}, + "=INDEX(A1:B2,2,3)": {"#REF!", "INDEX col_num out of range"}, + "=INDEX(A1:A2,0,0)": {"#VALUE!", "#VALUE!"}, + "=INDEX(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=INDEX(0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // INDIRECT - "=INDIRECT()": "INDIRECT requires 1 or 2 arguments", - "=INDIRECT(\"E\"&1,TRUE,1)": "INDIRECT requires 1 or 2 arguments", - "=INDIRECT(\"R1048577C1\",\"\")": "#VALUE!", - "=INDIRECT(\"E1048577\")": "#REF!", - "=INDIRECT(\"R1048577C1\",FALSE)": "#REF!", - "=INDIRECT(\"R1C16385\",FALSE)": "#REF!", - "=INDIRECT(\"\",FALSE)": "#REF!", - "=INDIRECT(\"R C1\",FALSE)": "#REF!", - "=INDIRECT(\"R1C \",FALSE)": "#REF!", - "=INDIRECT(\"R1C1:R2C \",FALSE)": "#REF!", + "=INDIRECT()": {"#VALUE!", "INDIRECT requires 1 or 2 arguments"}, + "=INDIRECT(\"E\"&1,TRUE,1)": {"#VALUE!", "INDIRECT requires 1 or 2 arguments"}, + "=INDIRECT(\"R1048577C1\",\"\")": {"#VALUE!", "#VALUE!"}, + "=INDIRECT(\"E1048577\")": {"#REF!", "#REF!"}, + "=INDIRECT(\"R1048577C1\",FALSE)": {"#REF!", "#REF!"}, + "=INDIRECT(\"R1C16385\",FALSE)": {"#REF!", "#REF!"}, + "=INDIRECT(\"\",FALSE)": {"#REF!", "#REF!"}, + "=INDIRECT(\"R C1\",FALSE)": {"#REF!", "#REF!"}, + "=INDIRECT(\"R1C \",FALSE)": {"#REF!", "#REF!"}, + "=INDIRECT(\"R1C1:R2C \",FALSE)": {"#REF!", "#REF!"}, // LOOKUP - "=LOOKUP()": "LOOKUP requires at least 2 arguments", - "=LOOKUP(D2,D1,D2)": "LOOKUP requires second argument of table array", - "=LOOKUP(D2,D1,D2,FALSE)": "LOOKUP requires at most 3 arguments", - "=LOOKUP(1,MUNIT(0))": "LOOKUP requires not empty range as second argument", - "=LOOKUP(D1,MUNIT(1),MUNIT(1))": "LOOKUP no result found", + "=LOOKUP()": {"#VALUE!", "LOOKUP requires at least 2 arguments"}, + "=LOOKUP(D2,D1,D2)": {"#VALUE!", "LOOKUP requires second argument of table array"}, + "=LOOKUP(D2,D1,D2,FALSE)": {"#VALUE!", "LOOKUP requires at most 3 arguments"}, + "=LOOKUP(1,MUNIT(0))": {"#VALUE!", "LOOKUP requires not empty range as second argument"}, + "=LOOKUP(D1,MUNIT(1),MUNIT(1))": {"#N/A", "LOOKUP no result found"}, // ROW - "=ROW(1,2)": "ROW requires at most 1 argument", - "=ROW(\"\")": "invalid reference", - "=ROW(Sheet1)": newInvalidColumnNameError("Sheet1").Error(), - "=ROW(Sheet1!A1!B1)": newInvalidColumnNameError("Sheet1").Error(), + "=ROW(1,2)": {"#VALUE!", "ROW requires at most 1 argument"}, + "=ROW(\"\")": {"#VALUE!", "invalid reference"}, + "=ROW(Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, + "=ROW(Sheet1!A1!B1)": {"", newInvalidColumnNameError("Sheet1").Error()}, // ROWS - "=ROWS()": "ROWS requires 1 argument", - "=ROWS(1)": "invalid reference", - "=ROWS(\"\")": "invalid reference", - "=ROWS(Sheet1)": newInvalidColumnNameError("Sheet1").Error(), - "=ROWS(Sheet1!A1!B1)": newInvalidColumnNameError("Sheet1").Error(), - "=ROWS(Sheet1!Sheet1)": newInvalidColumnNameError("Sheet1").Error(), + "=ROWS()": {"#VALUE!", "ROWS requires 1 argument"}, + "=ROWS(1)": {"#VALUE!", "invalid reference"}, + "=ROWS(\"\")": {"#VALUE!", "invalid reference"}, + "=ROWS(Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, + "=ROWS(Sheet1!A1!B1)": {"", newInvalidColumnNameError("Sheet1").Error()}, + "=ROWS(Sheet1!Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, // Web Functions // ENCODEURL - "=ENCODEURL()": "ENCODEURL requires 1 argument", + "=ENCODEURL()": {"#VALUE!", "ENCODEURL requires 1 argument"}, // Financial Functions // ACCRINT - "=ACCRINT()": "ACCRINT requires at least 6 arguments", - "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,1,FALSE,0)": "ACCRINT allows at most 8 arguments", - "=ACCRINT(\"\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,1,FALSE)": "#VALUE!", - "=ACCRINT(\"01/01/2012\",\"\",\"12/31/2013\",8%,10000,4,1,FALSE)": "#VALUE!", - "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"\",8%,10000,4,1,FALSE)": "#VALUE!", - "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",\"\",10000,4,1,FALSE)": "#NUM!", - "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,\"\",4,1,FALSE)": "#NUM!", - "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,3)": "#NUM!", - "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,\"\",1,FALSE)": "#NUM!", - "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,\"\",FALSE)": "#NUM!", - "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,1,\"\")": "#VALUE!", - "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,5,FALSE)": "invalid basis", + "=ACCRINT()": {"#VALUE!", "ACCRINT requires at least 6 arguments"}, + "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,1,FALSE,0)": {"#VALUE!", "ACCRINT allows at most 8 arguments"}, + "=ACCRINT(\"\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,1,FALSE)": {"#VALUE!", "#VALUE!"}, + "=ACCRINT(\"01/01/2012\",\"\",\"12/31/2013\",8%,10000,4,1,FALSE)": {"#VALUE!", "#VALUE!"}, + "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"\",8%,10000,4,1,FALSE)": {"#VALUE!", "#VALUE!"}, + "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",\"\",10000,4,1,FALSE)": {"#NUM!", "#NUM!"}, + "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,\"\",4,1,FALSE)": {"#NUM!", "#NUM!"}, + "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,3)": {"#NUM!", "#NUM!"}, + "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,\"\",1,FALSE)": {"#NUM!", "#NUM!"}, + "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,\"\",FALSE)": {"#NUM!", "#NUM!"}, + "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,1,\"\")": {"#VALUE!", "#VALUE!"}, + "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,5,FALSE)": {"#NUM!", "invalid basis"}, // ACCRINTM - "=ACCRINTM()": "ACCRINTM requires 4 or 5 arguments", - "=ACCRINTM(\"\",\"01/01/2012\",8%,10000)": "#VALUE!", - "=ACCRINTM(\"01/01/2012\",\"\",8%,10000)": "#VALUE!", - "=ACCRINTM(\"12/31/2012\",\"01/01/2012\",8%,10000)": "#NUM!", - "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",\"\",10000)": "#NUM!", - "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,\"\",10000)": "#NUM!", - "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,-1,10000)": "#NUM!", - "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000,\"\")": "#NUM!", - "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000,5)": "invalid basis", + "=ACCRINTM()": {"#VALUE!", "ACCRINTM requires 4 or 5 arguments"}, + "=ACCRINTM(\"\",\"01/01/2012\",8%,10000)": {"#VALUE!", "#VALUE!"}, + "=ACCRINTM(\"01/01/2012\",\"\",8%,10000)": {"#VALUE!", "#VALUE!"}, + "=ACCRINTM(\"12/31/2012\",\"01/01/2012\",8%,10000)": {"#NUM!", "#NUM!"}, + "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",\"\",10000)": {"#NUM!", "#NUM!"}, + "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,\"\",10000)": {"#NUM!", "#NUM!"}, + "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,-1,10000)": {"#NUM!", "#NUM!"}, + "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000,\"\")": {"#NUM!", "#NUM!"}, + "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000,5)": {"#NUM!", "invalid basis"}, // AMORDEGRC - "=AMORDEGRC()": "AMORDEGRC requires 6 or 7 arguments", - "=AMORDEGRC(\"\",\"01/01/2015\",\"09/30/2015\",20,1,20%)": "AMORDEGRC requires cost to be number argument", - "=AMORDEGRC(-1,\"01/01/2015\",\"09/30/2015\",20,1,20%)": "AMORDEGRC requires cost >= 0", - "=AMORDEGRC(150,\"\",\"09/30/2015\",20,1,20%)": "#VALUE!", - "=AMORDEGRC(150,\"01/01/2015\",\"\",20,1,20%)": "#VALUE!", - "=AMORDEGRC(150,\"09/30/2015\",\"01/01/2015\",20,1,20%)": "#NUM!", - "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",\"\",1,20%)": "#NUM!", - "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",-1,1,20%)": "#NUM!", - "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,\"\",20%)": "#NUM!", - "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,-1,20%)": "#NUM!", - "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,\"\")": "#NUM!", - "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,-1)": "#NUM!", - "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,\"\")": "#NUM!", - "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,50%)": "AMORDEGRC requires rate to be < 0.5", - "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,5)": "invalid basis", + "=AMORDEGRC()": {"#VALUE!", "AMORDEGRC requires 6 or 7 arguments"}, + "=AMORDEGRC(\"\",\"01/01/2015\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "AMORDEGRC requires cost to be number argument"}, + "=AMORDEGRC(-1,\"01/01/2015\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "AMORDEGRC requires cost >= 0"}, + "=AMORDEGRC(150,\"\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "#VALUE!"}, + "=AMORDEGRC(150,\"01/01/2015\",\"\",20,1,20%)": {"#VALUE!", "#VALUE!"}, + "=AMORDEGRC(150,\"09/30/2015\",\"01/01/2015\",20,1,20%)": {"#NUM!", "#NUM!"}, + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",\"\",1,20%)": {"#NUM!", "#NUM!"}, + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",-1,1,20%)": {"#NUM!", "#NUM!"}, + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,\"\",20%)": {"#NUM!", "#NUM!"}, + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,-1,20%)": {"#NUM!", "#NUM!"}, + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,\"\")": {"#NUM!", "#NUM!"}, + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,-1)": {"#NUM!", "#NUM!"}, + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,\"\")": {"#NUM!", "#NUM!"}, + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,50%)": {"#NUM!", "AMORDEGRC requires rate to be < 0.5"}, + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,5)": {"#NUM!", "invalid basis"}, // AMORLINC - "=AMORLINC()": "AMORLINC requires 6 or 7 arguments", - "=AMORLINC(\"\",\"01/01/2015\",\"09/30/2015\",20,1,20%)": "AMORLINC requires cost to be number argument", - "=AMORLINC(-1,\"01/01/2015\",\"09/30/2015\",20,1,20%)": "AMORLINC requires cost >= 0", - "=AMORLINC(150,\"\",\"09/30/2015\",20,1,20%)": "#VALUE!", - "=AMORLINC(150,\"01/01/2015\",\"\",20,1,20%)": "#VALUE!", - "=AMORLINC(150,\"09/30/2015\",\"01/01/2015\",20,1,20%)": "#NUM!", - "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",\"\",1,20%)": "#NUM!", - "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",-1,1,20%)": "#NUM!", - "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,\"\",20%)": "#NUM!", - "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,-1,20%)": "#NUM!", - "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,\"\")": "#NUM!", - "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,-1)": "#NUM!", - "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,\"\")": "#NUM!", - "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,5)": "invalid basis", + "=AMORLINC()": {"#VALUE!", "AMORLINC requires 6 or 7 arguments"}, + "=AMORLINC(\"\",\"01/01/2015\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "AMORLINC requires cost to be number argument"}, + "=AMORLINC(-1,\"01/01/2015\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "AMORLINC requires cost >= 0"}, + "=AMORLINC(150,\"\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "#VALUE!"}, + "=AMORLINC(150,\"01/01/2015\",\"\",20,1,20%)": {"#VALUE!", "#VALUE!"}, + "=AMORLINC(150,\"09/30/2015\",\"01/01/2015\",20,1,20%)": {"#NUM!", "#NUM!"}, + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",\"\",1,20%)": {"#NUM!", "#NUM!"}, + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",-1,1,20%)": {"#NUM!", "#NUM!"}, + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,\"\",20%)": {"#NUM!", "#NUM!"}, + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,-1,20%)": {"#NUM!", "#NUM!"}, + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,\"\")": {"#NUM!", "#NUM!"}, + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,-1)": {"#NUM!", "#NUM!"}, + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,\"\")": {"#NUM!", "#NUM!"}, + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,5)": {"#NUM!", "invalid basis"}, // COUPDAYBS - "=COUPDAYBS()": "COUPDAYBS requires 3 or 4 arguments", - "=COUPDAYBS(\"\",\"10/25/2012\",4)": "#VALUE!", - "=COUPDAYBS(\"01/01/2011\",\"\",4)": "#VALUE!", - "=COUPDAYBS(\"01/01/2011\",\"10/25/2012\",\"\")": "#VALUE!", - "=COUPDAYBS(\"01/01/2011\",\"10/25/2012\",4,\"\")": "#NUM!", - "=COUPDAYBS(\"10/25/2012\",\"01/01/2011\",4)": "COUPDAYBS requires maturity > settlement", + "=COUPDAYBS()": {"#VALUE!", "COUPDAYBS requires 3 or 4 arguments"}, + "=COUPDAYBS(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPDAYBS(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPDAYBS(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"}, + "=COUPDAYBS(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"}, + "=COUPDAYBS(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPDAYBS requires maturity > settlement"}, // COUPDAYS - "=COUPDAYS()": "COUPDAYS requires 3 or 4 arguments", - "=COUPDAYS(\"\",\"10/25/2012\",4)": "#VALUE!", - "=COUPDAYS(\"01/01/2011\",\"\",4)": "#VALUE!", - "=COUPDAYS(\"01/01/2011\",\"10/25/2012\",\"\")": "#VALUE!", - "=COUPDAYS(\"01/01/2011\",\"10/25/2012\",4,\"\")": "#NUM!", - "=COUPDAYS(\"10/25/2012\",\"01/01/2011\",4)": "COUPDAYS requires maturity > settlement", + "=COUPDAYS()": {"#VALUE!", "COUPDAYS requires 3 or 4 arguments"}, + "=COUPDAYS(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPDAYS(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPDAYS(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"}, + "=COUPDAYS(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"}, + "=COUPDAYS(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPDAYS requires maturity > settlement"}, // COUPDAYSNC - "=COUPDAYSNC()": "COUPDAYSNC requires 3 or 4 arguments", - "=COUPDAYSNC(\"\",\"10/25/2012\",4)": "#VALUE!", - "=COUPDAYSNC(\"01/01/2011\",\"\",4)": "#VALUE!", - "=COUPDAYSNC(\"01/01/2011\",\"10/25/2012\",\"\")": "#VALUE!", - "=COUPDAYSNC(\"01/01/2011\",\"10/25/2012\",4,\"\")": "#NUM!", - "=COUPDAYSNC(\"10/25/2012\",\"01/01/2011\",4)": "COUPDAYSNC requires maturity > settlement", + "=COUPDAYSNC()": {"#VALUE!", "COUPDAYSNC requires 3 or 4 arguments"}, + "=COUPDAYSNC(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPDAYSNC(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPDAYSNC(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"}, + "=COUPDAYSNC(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"}, + "=COUPDAYSNC(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPDAYSNC requires maturity > settlement"}, // COUPNCD - "=COUPNCD()": "COUPNCD requires 3 or 4 arguments", - "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4,0,0)": "COUPNCD requires 3 or 4 arguments", - "=COUPNCD(\"\",\"10/25/2012\",4)": "#VALUE!", - "=COUPNCD(\"01/01/2011\",\"\",4)": "#VALUE!", - "=COUPNCD(\"01/01/2011\",\"10/25/2012\",\"\")": "#VALUE!", - "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4,\"\")": "#NUM!", - "=COUPNCD(\"01/01/2011\",\"10/25/2012\",3)": "#NUM!", - "=COUPNCD(\"10/25/2012\",\"01/01/2011\",4)": "COUPNCD requires maturity > settlement", + "=COUPNCD()": {"#VALUE!", "COUPNCD requires 3 or 4 arguments"}, + "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4,0,0)": {"#VALUE!", "COUPNCD requires 3 or 4 arguments"}, + "=COUPNCD(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPNCD(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPNCD(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"}, + "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"}, + "=COUPNCD(\"01/01/2011\",\"10/25/2012\",3)": {"#NUM!", "#NUM!"}, + "=COUPNCD(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPNCD requires maturity > settlement"}, // COUPNUM - "=COUPNUM()": "COUPNUM requires 3 or 4 arguments", - "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4,0,0)": "COUPNUM requires 3 or 4 arguments", - "=COUPNUM(\"\",\"10/25/2012\",4)": "#VALUE!", - "=COUPNUM(\"01/01/2011\",\"\",4)": "#VALUE!", - "=COUPNUM(\"01/01/2011\",\"10/25/2012\",\"\")": "#VALUE!", - "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4,\"\")": "#NUM!", - "=COUPNUM(\"01/01/2011\",\"10/25/2012\",3)": "#NUM!", - "=COUPNUM(\"10/25/2012\",\"01/01/2011\",4)": "COUPNUM requires maturity > settlement", + "=COUPNUM()": {"#VALUE!", "COUPNUM requires 3 or 4 arguments"}, + "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4,0,0)": {"#VALUE!", "COUPNUM requires 3 or 4 arguments"}, + "=COUPNUM(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPNUM(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPNUM(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"}, + "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"}, + "=COUPNUM(\"01/01/2011\",\"10/25/2012\",3)": {"#NUM!", "#NUM!"}, + "=COUPNUM(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPNUM requires maturity > settlement"}, // COUPPCD - "=COUPPCD()": "COUPPCD requires 3 or 4 arguments", - "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,0,0)": "COUPPCD requires 3 or 4 arguments", - "=COUPPCD(\"\",\"10/25/2012\",4)": "#VALUE!", - "=COUPPCD(\"01/01/2011\",\"\",4)": "#VALUE!", - "=COUPPCD(\"01/01/2011\",\"10/25/2012\",\"\")": "#VALUE!", - "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,\"\")": "#NUM!", - "=COUPPCD(\"01/01/2011\",\"10/25/2012\",3)": "#NUM!", - "=COUPPCD(\"10/25/2012\",\"01/01/2011\",4)": "COUPPCD requires maturity > settlement", + "=COUPPCD()": {"#VALUE!", "COUPPCD requires 3 or 4 arguments"}, + "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,0,0)": {"#VALUE!", "COUPPCD requires 3 or 4 arguments"}, + "=COUPPCD(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPPCD(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"}, + "=COUPPCD(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"}, + "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"}, + "=COUPPCD(\"01/01/2011\",\"10/25/2012\",3)": {"#NUM!", "#NUM!"}, + "=COUPPCD(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPPCD requires maturity > settlement"}, // CUMIPMT - "=CUMIPMT()": "CUMIPMT requires 6 arguments", - "=CUMIPMT(0,0,0,0,0,2)": "#N/A", - "=CUMIPMT(0,0,0,-1,0,0)": "#N/A", - "=CUMIPMT(0,0,0,1,0,0)": "#N/A", - "=CUMIPMT(\"\",0,0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CUMIPMT(0,\"\",0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CUMIPMT(0,0,\"\",0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CUMIPMT(0,0,0,\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CUMIPMT(0,0,0,0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CUMIPMT(0,0,0,0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CUMIPMT()": {"#VALUE!", "CUMIPMT requires 6 arguments"}, + "=CUMIPMT(0,0,0,0,0,2)": {"#N/A", "#N/A"}, + "=CUMIPMT(0,0,0,-1,0,0)": {"#N/A", "#N/A"}, + "=CUMIPMT(0,0,0,1,0,0)": {"#N/A", "#N/A"}, + "=CUMIPMT(\"\",0,0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CUMIPMT(0,\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CUMIPMT(0,0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CUMIPMT(0,0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CUMIPMT(0,0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CUMIPMT(0,0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // CUMPRINC - "=CUMPRINC()": "CUMPRINC requires 6 arguments", - "=CUMPRINC(0,0,0,0,0,2)": "#N/A", - "=CUMPRINC(0,0,0,-1,0,0)": "#N/A", - "=CUMPRINC(0,0,0,1,0,0)": "#N/A", - "=CUMPRINC(\"\",0,0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CUMPRINC(0,\"\",0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CUMPRINC(0,0,\"\",0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CUMPRINC(0,0,0,\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CUMPRINC(0,0,0,0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=CUMPRINC(0,0,0,0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CUMPRINC()": {"#VALUE!", "CUMPRINC requires 6 arguments"}, + "=CUMPRINC(0,0,0,0,0,2)": {"#N/A", "#N/A"}, + "=CUMPRINC(0,0,0,-1,0,0)": {"#N/A", "#N/A"}, + "=CUMPRINC(0,0,0,1,0,0)": {"#N/A", "#N/A"}, + "=CUMPRINC(\"\",0,0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CUMPRINC(0,\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CUMPRINC(0,0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CUMPRINC(0,0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CUMPRINC(0,0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=CUMPRINC(0,0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // DB - "=DB()": "DB requires at least 4 arguments", - "=DB(0,0,0,0,0,0)": "DB allows at most 5 arguments", - "=DB(-1,0,0,0)": "#N/A", - "=DB(\"\",0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DB(0,\"\",0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DB(0,0,\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DB(0,0,0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DB(0,0,0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=DB()": {"#VALUE!", "DB requires at least 4 arguments"}, + "=DB(0,0,0,0,0,0)": {"#VALUE!", "DB allows at most 5 arguments"}, + "=DB(-1,0,0,0)": {"#N/A", "#N/A"}, + "=DB(\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DB(0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DB(0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DB(0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DB(0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // DDB - "=DDB()": "DDB requires at least 4 arguments", - "=DDB(0,0,0,0,0,0)": "DDB allows at most 5 arguments", - "=DDB(-1,0,0,0)": "#N/A", - "=DDB(\"\",0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DDB(0,\"\",0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DDB(0,0,\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DDB(0,0,0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DDB(0,0,0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=DDB()": {"#VALUE!", "DDB requires at least 4 arguments"}, + "=DDB(0,0,0,0,0,0)": {"#VALUE!", "DDB allows at most 5 arguments"}, + "=DDB(-1,0,0,0)": {"#N/A", "#N/A"}, + "=DDB(\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DDB(0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DDB(0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DDB(0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DDB(0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // DISC - "=DISC()": "DISC requires 4 or 5 arguments", - "=DISC(\"\",\"03/31/2021\",95,100)": "#VALUE!", - "=DISC(\"04/01/2016\",\"\",95,100)": "#VALUE!", - "=DISC(\"04/01/2016\",\"03/31/2021\",\"\",100)": "#VALUE!", - "=DISC(\"04/01/2016\",\"03/31/2021\",95,\"\")": "#VALUE!", - "=DISC(\"04/01/2016\",\"03/31/2021\",95,100,\"\")": "#NUM!", - "=DISC(\"03/31/2021\",\"04/01/2016\",95,100)": "DISC requires maturity > settlement", - "=DISC(\"04/01/2016\",\"03/31/2021\",0,100)": "DISC requires pr > 0", - "=DISC(\"04/01/2016\",\"03/31/2021\",95,0)": "DISC requires redemption > 0", - "=DISC(\"04/01/2016\",\"03/31/2021\",95,100,5)": "invalid basis", + "=DISC()": {"#VALUE!", "DISC requires 4 or 5 arguments"}, + "=DISC(\"\",\"03/31/2021\",95,100)": {"#VALUE!", "#VALUE!"}, + "=DISC(\"04/01/2016\",\"\",95,100)": {"#VALUE!", "#VALUE!"}, + "=DISC(\"04/01/2016\",\"03/31/2021\",\"\",100)": {"#VALUE!", "#VALUE!"}, + "=DISC(\"04/01/2016\",\"03/31/2021\",95,\"\")": {"#VALUE!", "#VALUE!"}, + "=DISC(\"04/01/2016\",\"03/31/2021\",95,100,\"\")": {"#NUM!", "#NUM!"}, + "=DISC(\"03/31/2021\",\"04/01/2016\",95,100)": {"#NUM!", "DISC requires maturity > settlement"}, + "=DISC(\"04/01/2016\",\"03/31/2021\",0,100)": {"#NUM!", "DISC requires pr > 0"}, + "=DISC(\"04/01/2016\",\"03/31/2021\",95,0)": {"#NUM!", "DISC requires redemption > 0"}, + "=DISC(\"04/01/2016\",\"03/31/2021\",95,100,5)": {"#NUM!", "invalid basis"}, // DOLLARDE - "=DOLLARDE()": "DOLLARDE requires 2 arguments", - "=DOLLARDE(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DOLLARDE(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DOLLARDE(0,-1)": "#NUM!", - "=DOLLARDE(0,0)": "#DIV/0!", + "=DOLLARDE()": {"#VALUE!", "DOLLARDE requires 2 arguments"}, + "=DOLLARDE(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DOLLARDE(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DOLLARDE(0,-1)": {"#NUM!", "#NUM!"}, + "=DOLLARDE(0,0)": {"#DIV/0!", "#DIV/0!"}, // DOLLARFR - "=DOLLARFR()": "DOLLARFR requires 2 arguments", - "=DOLLARFR(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DOLLARFR(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DOLLARFR(0,-1)": "#NUM!", - "=DOLLARFR(0,0)": "#DIV/0!", + "=DOLLARFR()": {"#VALUE!", "DOLLARFR requires 2 arguments"}, + "=DOLLARFR(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DOLLARFR(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DOLLARFR(0,-1)": {"#NUM!", "#NUM!"}, + "=DOLLARFR(0,0)": {"#DIV/0!", "#DIV/0!"}, // DURATION - "=DURATION()": "DURATION requires 5 or 6 arguments", - "=DURATION(\"\",\"03/31/2025\",10%,8%,4)": "#VALUE!", - "=DURATION(\"04/01/2015\",\"\",10%,8%,4)": "#VALUE!", - "=DURATION(\"03/31/2025\",\"04/01/2015\",10%,8%,4)": "DURATION requires maturity > settlement", - "=DURATION(\"04/01/2015\",\"03/31/2025\",-1,8%,4)": "DURATION requires coupon >= 0", - "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,-1,4)": "DURATION requires yld >= 0", - "=DURATION(\"04/01/2015\",\"03/31/2025\",\"\",8%,4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,\"\",4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,3)": "#NUM!", - "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,\"\")": "#NUM!", - "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,5)": "invalid basis", + "=DURATION()": {"#VALUE!", "DURATION requires 5 or 6 arguments"}, + "=DURATION(\"\",\"03/31/2025\",10%,8%,4)": {"#VALUE!", "#VALUE!"}, + "=DURATION(\"04/01/2015\",\"\",10%,8%,4)": {"#VALUE!", "#VALUE!"}, + "=DURATION(\"03/31/2025\",\"04/01/2015\",10%,8%,4)": {"#NUM!", "DURATION requires maturity > settlement"}, + "=DURATION(\"04/01/2015\",\"03/31/2025\",-1,8%,4)": {"#NUM!", "DURATION requires coupon >= 0"}, + "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,-1,4)": {"#NUM!", "DURATION requires yld >= 0"}, + "=DURATION(\"04/01/2015\",\"03/31/2025\",\"\",8%,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,\"\",4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,3)": {"#NUM!", "#NUM!"}, + "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,\"\")": {"#NUM!", "#NUM!"}, + "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,5)": {"#NUM!", "invalid basis"}, // EFFECT - "=EFFECT()": "EFFECT requires 2 arguments", - "=EFFECT(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=EFFECT(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=EFFECT(0,0)": "#NUM!", - "=EFFECT(1,0)": "#NUM!", + "=EFFECT()": {"#VALUE!", "EFFECT requires 2 arguments"}, + "=EFFECT(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=EFFECT(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=EFFECT(0,0)": {"#NUM!", "#NUM!"}, + "=EFFECT(1,0)": {"#NUM!", "#NUM!"}, // EUROCONVERT - "=EUROCONVERT()": "EUROCONVERT requires at least 3 arguments", - "=EUROCONVERT(1.47,\"FRF\",\"DEM\",TRUE,3,1)": "EUROCONVERT allows at most 5 arguments", - "=EUROCONVERT(\"\",\"FRF\",\"DEM\",TRUE,3)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=EUROCONVERT(1.47,\"FRF\",\"DEM\",\"\",3)": "strconv.ParseBool: parsing \"\": invalid syntax", - "=EUROCONVERT(1.47,\"FRF\",\"DEM\",TRUE,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=EUROCONVERT(1.47,\"\",\"DEM\")": "#VALUE!", - "=EUROCONVERT(1.47,\"FRF\",\"\",TRUE,3)": "#VALUE!", + "=EUROCONVERT()": {"#VALUE!", "EUROCONVERT requires at least 3 arguments"}, + "=EUROCONVERT(1.47,\"FRF\",\"DEM\",TRUE,3,1)": {"#VALUE!", "EUROCONVERT allows at most 5 arguments"}, + "=EUROCONVERT(\"\",\"FRF\",\"DEM\",TRUE,3)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=EUROCONVERT(1.47,\"FRF\",\"DEM\",\"\",3)": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=EUROCONVERT(1.47,\"FRF\",\"DEM\",TRUE,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=EUROCONVERT(1.47,\"\",\"DEM\")": {"#VALUE!", "#VALUE!"}, + "=EUROCONVERT(1.47,\"FRF\",\"\",TRUE,3)": {"#VALUE!", "#VALUE!"}, // FV - "=FV()": "FV requires at least 3 arguments", - "=FV(0,0,0,0,0,0,0)": "FV allows at most 5 arguments", - "=FV(0,0,0,0,2)": "#N/A", - "=FV(\"\",0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FV(0,\"\",0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FV(0,0,\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FV(0,0,0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FV(0,0,0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=FV()": {"#VALUE!", "FV requires at least 3 arguments"}, + "=FV(0,0,0,0,0,0,0)": {"#VALUE!", "FV allows at most 5 arguments"}, + "=FV(0,0,0,0,2)": {"#N/A", "#N/A"}, + "=FV(\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FV(0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FV(0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FV(0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FV(0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // FVSCHEDULE - "=FVSCHEDULE()": "FVSCHEDULE requires 2 arguments", - "=FVSCHEDULE(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=FVSCHEDULE(0,\"x\")": "strconv.ParseFloat: parsing \"x\": invalid syntax", + "=FVSCHEDULE()": {"#VALUE!", "FVSCHEDULE requires 2 arguments"}, + "=FVSCHEDULE(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FVSCHEDULE(0,\"x\")": {"#VALUE!", "strconv.ParseFloat: parsing \"x\": invalid syntax"}, // INTRATE - "=INTRATE()": "INTRATE requires 4 or 5 arguments", - "=INTRATE(\"\",\"03/31/2021\",95,100)": "#VALUE!", - "=INTRATE(\"04/01/2016\",\"\",95,100)": "#VALUE!", - "=INTRATE(\"04/01/2016\",\"03/31/2021\",\"\",100)": "#VALUE!", - "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,\"\")": "#VALUE!", - "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,100,\"\")": "#NUM!", - "=INTRATE(\"03/31/2021\",\"04/01/2016\",95,100)": "INTRATE requires maturity > settlement", - "=INTRATE(\"04/01/2016\",\"03/31/2021\",0,100)": "INTRATE requires investment > 0", - "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,0)": "INTRATE requires redemption > 0", - "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,100,5)": "invalid basis", + "=INTRATE()": {"#VALUE!", "INTRATE requires 4 or 5 arguments"}, + "=INTRATE(\"\",\"03/31/2021\",95,100)": {"#VALUE!", "#VALUE!"}, + "=INTRATE(\"04/01/2016\",\"\",95,100)": {"#VALUE!", "#VALUE!"}, + "=INTRATE(\"04/01/2016\",\"03/31/2021\",\"\",100)": {"#VALUE!", "#VALUE!"}, + "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,\"\")": {"#VALUE!", "#VALUE!"}, + "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,100,\"\")": {"#NUM!", "#NUM!"}, + "=INTRATE(\"03/31/2021\",\"04/01/2016\",95,100)": {"#NUM!", "INTRATE requires maturity > settlement"}, + "=INTRATE(\"04/01/2016\",\"03/31/2021\",0,100)": {"#NUM!", "INTRATE requires investment > 0"}, + "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,0)": {"#NUM!", "INTRATE requires redemption > 0"}, + "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,100,5)": {"#NUM!", "invalid basis"}, // IPMT - "=IPMT()": "IPMT requires at least 4 arguments", - "=IPMT(0,0,0,0,0,0,0)": "IPMT allows at most 6 arguments", - "=IPMT(0,0,0,0,0,2)": "#N/A", - "=IPMT(0,-1,0,0,0,0)": "#N/A", - "=IPMT(0,1,0,0,0,0)": "#N/A", - "=IPMT(\"\",0,0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=IPMT(0,\"\",0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=IPMT(0,0,\"\",0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=IPMT(0,0,0,\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=IPMT(0,0,0,0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=IPMT(0,0,0,0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=IPMT()": {"#VALUE!", "IPMT requires at least 4 arguments"}, + "=IPMT(0,0,0,0,0,0,0)": {"#VALUE!", "IPMT allows at most 6 arguments"}, + "=IPMT(0,0,0,0,0,2)": {"#N/A", "#N/A"}, + "=IPMT(0,-1,0,0,0,0)": {"#N/A", "#N/A"}, + "=IPMT(0,1,0,0,0,0)": {"#N/A", "#N/A"}, + "=IPMT(\"\",0,0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=IPMT(0,\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=IPMT(0,0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=IPMT(0,0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=IPMT(0,0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=IPMT(0,0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // ISPMT - "=ISPMT()": "ISPMT requires 4 arguments", - "=ISPMT(\"\",0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=ISPMT(0,\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=ISPMT(0,0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=ISPMT(0,0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=ISPMT()": {"#VALUE!", "ISPMT requires 4 arguments"}, + "=ISPMT(\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ISPMT(0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ISPMT(0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ISPMT(0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // MDURATION - "=MDURATION()": "MDURATION requires 5 or 6 arguments", - "=MDURATION(\"\",\"03/31/2025\",10%,8%,4)": "#VALUE!", - "=MDURATION(\"04/01/2015\",\"\",10%,8%,4)": "#VALUE!", - "=MDURATION(\"03/31/2025\",\"04/01/2015\",10%,8%,4)": "MDURATION requires maturity > settlement", - "=MDURATION(\"04/01/2015\",\"03/31/2025\",-1,8%,4)": "MDURATION requires coupon >= 0", - "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,-1,4)": "MDURATION requires yld >= 0", - "=MDURATION(\"04/01/2015\",\"03/31/2025\",\"\",8%,4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,\"\",4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,3)": "#NUM!", - "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,\"\")": "#NUM!", - "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,5)": "invalid basis", + "=MDURATION()": {"#VALUE!", "MDURATION requires 5 or 6 arguments"}, + "=MDURATION(\"\",\"03/31/2025\",10%,8%,4)": {"#VALUE!", "#VALUE!"}, + "=MDURATION(\"04/01/2015\",\"\",10%,8%,4)": {"#VALUE!", "#VALUE!"}, + "=MDURATION(\"03/31/2025\",\"04/01/2015\",10%,8%,4)": {"#NUM!", "MDURATION requires maturity > settlement"}, + "=MDURATION(\"04/01/2015\",\"03/31/2025\",-1,8%,4)": {"#NUM!", "MDURATION requires coupon >= 0"}, + "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,-1,4)": {"#NUM!", "MDURATION requires yld >= 0"}, + "=MDURATION(\"04/01/2015\",\"03/31/2025\",\"\",8%,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,\"\",4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,3)": {"#NUM!", "#NUM!"}, + "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,\"\")": {"#NUM!", "#NUM!"}, + "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,5)": {"#NUM!", "invalid basis"}, // NOMINAL - "=NOMINAL()": "NOMINAL requires 2 arguments", - "=NOMINAL(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NOMINAL(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NOMINAL(0,0)": "#NUM!", - "=NOMINAL(1,0)": "#NUM!", + "=NOMINAL()": {"#VALUE!", "NOMINAL requires 2 arguments"}, + "=NOMINAL(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NOMINAL(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NOMINAL(0,0)": {"#NUM!", "#NUM!"}, + "=NOMINAL(1,0)": {"#NUM!", "#NUM!"}, // NPER - "=NPER()": "NPER requires at least 3 arguments", - "=NPER(0,0,0,0,0,0)": "NPER allows at most 5 arguments", - "=NPER(0,0,0)": "#NUM!", - "=NPER(0,0,0,0,2)": "#N/A", - "=NPER(\"\",0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NPER(0,\"\",0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NPER(0,0,\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NPER(0,0,0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=NPER(0,0,0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=NPER()": {"#VALUE!", "NPER requires at least 3 arguments"}, + "=NPER(0,0,0,0,0,0)": {"#VALUE!", "NPER allows at most 5 arguments"}, + "=NPER(0,0,0)": {"#NUM!", "#NUM!"}, + "=NPER(0,0,0,0,2)": {"#N/A", "#N/A"}, + "=NPER(\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NPER(0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NPER(0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NPER(0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=NPER(0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // NPV - "=NPV()": "NPV requires at least 2 arguments", - "=NPV(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=NPV()": {"#VALUE!", "NPV requires at least 2 arguments"}, + "=NPV(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // ODDFPRICE - "=ODDFPRICE()": "ODDFPRICE requires 8 or 9 arguments", - "=ODDFPRICE(\"\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": "#VALUE!", - "=ODDFPRICE(\"02/01/2017\",\"\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": "#VALUE!", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"\",\"03/31/2017\",5.5%,3.5%,100,2)": "#VALUE!", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"\",5.5%,3.5%,100,2)": "#VALUE!", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",\"\",3.5%,100,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,\"\",100,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"02/01/2017\",\"03/31/2017\",5.5%,3.5%,100,2)": "ODDFPRICE requires settlement > issue", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"02/01/2017\",5.5%,3.5%,100,2)": "ODDFPRICE requires first_coupon > settlement", - "=ODDFPRICE(\"02/01/2017\",\"02/01/2017\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": "ODDFPRICE requires maturity > first_coupon", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",-1,3.5%,100,2)": "ODDFPRICE requires rate >= 0", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,-1,100,2)": "ODDFPRICE requires yld >= 0", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,0,2)": "ODDFPRICE requires redemption > 0", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,\"\")": "#NUM!", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,3)": "#NUM!", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/30/2017\",5.5%,3.5%,100,4)": "#NUM!", - "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,5)": "invalid basis", + "=ODDFPRICE()": {"#VALUE!", "ODDFPRICE requires 8 or 9 arguments"}, + "=ODDFPRICE(\"\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDFPRICE(\"02/01/2017\",\"\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",\"\",3.5%,100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,\"\",100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"02/01/2017\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFPRICE requires settlement > issue"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"02/01/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFPRICE requires first_coupon > settlement"}, + "=ODDFPRICE(\"02/01/2017\",\"02/01/2017\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFPRICE requires maturity > first_coupon"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",-1,3.5%,100,2)": {"#NUM!", "ODDFPRICE requires rate >= 0"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,-1,100,2)": {"#NUM!", "ODDFPRICE requires yld >= 0"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,0,2)": {"#NUM!", "ODDFPRICE requires redemption > 0"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,\"\")": {"#NUM!", "#NUM!"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,3)": {"#NUM!", "#NUM!"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/30/2017\",5.5%,3.5%,100,4)": {"#NUM!", "#NUM!"}, + "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,5)": {"#NUM!", "invalid basis"}, // PDURATION - "=PDURATION()": "PDURATION requires 3 arguments", - "=PDURATION(\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PDURATION(0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PDURATION(0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PDURATION(0,0,0)": "#NUM!", + "=PDURATION()": {"#VALUE!", "PDURATION requires 3 arguments"}, + "=PDURATION(\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PDURATION(0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PDURATION(0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PDURATION(0,0,0)": {"#NUM!", "#NUM!"}, // PMT - "=PMT()": "PMT requires at least 3 arguments", - "=PMT(0,0,0,0,0,0)": "PMT allows at most 5 arguments", - "=PMT(0,0,0,0,2)": "#N/A", - "=PMT(\"\",0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PMT(0,\"\",0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PMT(0,0,\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PMT(0,0,0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PMT(0,0,0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=PMT()": {"#VALUE!", "PMT requires at least 3 arguments"}, + "=PMT(0,0,0,0,0,0)": {"#VALUE!", "PMT allows at most 5 arguments"}, + "=PMT(0,0,0,0,2)": {"#N/A", "#N/A"}, + "=PMT(\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PMT(0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PMT(0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PMT(0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PMT(0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // PRICE - "=PRICE()": "PRICE requires 6 or 7 arguments", - "=PRICE(\"\",\"02/01/2020\",12%,10%,100,2,4)": "#VALUE!", - "=PRICE(\"04/01/2012\",\"\",12%,10%,100,2,4)": "#VALUE!", - "=PRICE(\"04/01/2012\",\"02/01/2020\",\"\",10%,100,2,4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,\"\",100,2,4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,\"\",2,4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,\"\",4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PRICE(\"04/01/2012\",\"02/01/2020\",-1,10%,100,2,4)": "PRICE requires rate >= 0", - "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,-1,100,2,4)": "PRICE requires yld >= 0", - "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,0,2,4)": "PRICE requires redemption > 0", - "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2,\"\")": "#NUM!", - "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,3,4)": "#NUM!", - "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2,5)": "invalid basis", + "=PRICE()": {"#VALUE!", "PRICE requires 6 or 7 arguments"}, + "=PRICE(\"\",\"02/01/2020\",12%,10%,100,2,4)": {"#VALUE!", "#VALUE!"}, + "=PRICE(\"04/01/2012\",\"\",12%,10%,100,2,4)": {"#VALUE!", "#VALUE!"}, + "=PRICE(\"04/01/2012\",\"02/01/2020\",\"\",10%,100,2,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,\"\",100,2,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,\"\",2,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,\"\",4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PRICE(\"04/01/2012\",\"02/01/2020\",-1,10%,100,2,4)": {"#NUM!", "PRICE requires rate >= 0"}, + "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,-1,100,2,4)": {"#NUM!", "PRICE requires yld >= 0"}, + "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,0,2,4)": {"#NUM!", "PRICE requires redemption > 0"}, + "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2,\"\")": {"#NUM!", "#NUM!"}, + "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,3,4)": {"#NUM!", "#NUM!"}, + "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2,5)": {"#NUM!", "invalid basis"}, // PPMT - "=PPMT()": "PPMT requires at least 4 arguments", - "=PPMT(0,0,0,0,0,0,0)": "PPMT allows at most 6 arguments", - "=PPMT(0,0,0,0,0,2)": "#N/A", - "=PPMT(0,-1,0,0,0,0)": "#N/A", - "=PPMT(0,1,0,0,0,0)": "#N/A", - "=PPMT(\"\",0,0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PPMT(0,\"\",0,0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PPMT(0,0,\"\",0,0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PPMT(0,0,0,\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PPMT(0,0,0,0,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PPMT(0,0,0,0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=PPMT()": {"#VALUE!", "PPMT requires at least 4 arguments"}, + "=PPMT(0,0,0,0,0,0,0)": {"#VALUE!", "PPMT allows at most 6 arguments"}, + "=PPMT(0,0,0,0,0,2)": {"#N/A", "#N/A"}, + "=PPMT(0,-1,0,0,0,0)": {"#N/A", "#N/A"}, + "=PPMT(0,1,0,0,0,0)": {"#N/A", "#N/A"}, + "=PPMT(\"\",0,0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PPMT(0,\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PPMT(0,0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PPMT(0,0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PPMT(0,0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PPMT(0,0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // PRICEDISC - "=PRICEDISC()": "PRICEDISC requires 4 or 5 arguments", - "=PRICEDISC(\"\",\"03/31/2021\",95,100)": "#VALUE!", - "=PRICEDISC(\"04/01/2016\",\"\",95,100)": "#VALUE!", - "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",\"\",100)": "#VALUE!", - "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,\"\")": "#VALUE!", - "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,100,\"\")": "#NUM!", - "=PRICEDISC(\"03/31/2021\",\"04/01/2016\",95,100)": "PRICEDISC requires maturity > settlement", - "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",0,100)": "PRICEDISC requires discount > 0", - "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,0)": "PRICEDISC requires redemption > 0", - "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,100,5)": "invalid basis", + "=PRICEDISC()": {"#VALUE!", "PRICEDISC requires 4 or 5 arguments"}, + "=PRICEDISC(\"\",\"03/31/2021\",95,100)": {"#VALUE!", "#VALUE!"}, + "=PRICEDISC(\"04/01/2016\",\"\",95,100)": {"#VALUE!", "#VALUE!"}, + "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",\"\",100)": {"#VALUE!", "#VALUE!"}, + "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,\"\")": {"#VALUE!", "#VALUE!"}, + "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,100,\"\")": {"#NUM!", "#NUM!"}, + "=PRICEDISC(\"03/31/2021\",\"04/01/2016\",95,100)": {"#NUM!", "PRICEDISC requires maturity > settlement"}, + "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",0,100)": {"#NUM!", "PRICEDISC requires discount > 0"}, + "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,0)": {"#NUM!", "PRICEDISC requires redemption > 0"}, + "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,100,5)": {"#NUM!", "invalid basis"}, // PRICEMAT - "=PRICEMAT()": "PRICEMAT requires 5 or 6 arguments", - "=PRICEMAT(\"\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%)": "#VALUE!", - "=PRICEMAT(\"04/01/2017\",\"\",\"01/01/2017\",4.5%,2.5%)": "#VALUE!", - "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"\",4.5%,2.5%)": "#VALUE!", - "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",\"\",2.5%)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%,\"\")": "#NUM!", - "=PRICEMAT(\"03/31/2021\",\"04/01/2017\",\"01/01/2017\",4.5%,2.5%)": "PRICEMAT requires maturity > settlement", - "=PRICEMAT(\"01/01/2017\",\"03/31/2021\",\"04/01/2017\",4.5%,2.5%)": "PRICEMAT requires settlement > issue", - "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",-1,2.5%)": "PRICEMAT requires rate >= 0", - "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,-1)": "PRICEMAT requires yld >= 0", - "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%,5)": "invalid basis", + "=PRICEMAT()": {"#VALUE!", "PRICEMAT requires 5 or 6 arguments"}, + "=PRICEMAT(\"\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%)": {"#VALUE!", "#VALUE!"}, + "=PRICEMAT(\"04/01/2017\",\"\",\"01/01/2017\",4.5%,2.5%)": {"#VALUE!", "#VALUE!"}, + "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"\",4.5%,2.5%)": {"#VALUE!", "#VALUE!"}, + "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",\"\",2.5%)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%,\"\")": {"#NUM!", "#NUM!"}, + "=PRICEMAT(\"03/31/2021\",\"04/01/2017\",\"01/01/2017\",4.5%,2.5%)": {"#NUM!", "PRICEMAT requires maturity > settlement"}, + "=PRICEMAT(\"01/01/2017\",\"03/31/2021\",\"04/01/2017\",4.5%,2.5%)": {"#NUM!", "PRICEMAT requires settlement > issue"}, + "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",-1,2.5%)": {"#NUM!", "PRICEMAT requires rate >= 0"}, + "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,-1)": {"#NUM!", "PRICEMAT requires yld >= 0"}, + "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%,5)": {"#NUM!", "invalid basis"}, // PV - "=PV()": "PV requires at least 3 arguments", - "=PV(10%/4,16,2000,0,1,0)": "PV allows at most 5 arguments", - "=PV(\"\",16,2000,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PV(10%/4,\"\",2000,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PV(10%/4,16,\"\",0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PV(10%/4,16,2000,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=PV(10%/4,16,2000,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=PV()": {"#VALUE!", "PV requires at least 3 arguments"}, + "=PV(10%/4,16,2000,0,1,0)": {"#VALUE!", "PV allows at most 5 arguments"}, + "=PV(\"\",16,2000,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PV(10%/4,\"\",2000,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PV(10%/4,16,\"\",0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PV(10%/4,16,2000,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=PV(10%/4,16,2000,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // RATE - "=RATE()": "RATE requires at least 3 arguments", - "=RATE(48,-200,8000,3,1,0.5,0)": "RATE allows at most 6 arguments", - "=RATE(\"\",-200,8000,3,1,0.5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=RATE(48,\"\",8000,3,1,0.5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=RATE(48,-200,\"\",3,1,0.5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=RATE(48,-200,8000,\"\",1,0.5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=RATE(48,-200,8000,3,\"\",0.5)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=RATE(48,-200,8000,3,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=RATE()": {"#VALUE!", "RATE requires at least 3 arguments"}, + "=RATE(48,-200,8000,3,1,0.5,0)": {"#VALUE!", "RATE allows at most 6 arguments"}, + "=RATE(\"\",-200,8000,3,1,0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=RATE(48,\"\",8000,3,1,0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=RATE(48,-200,\"\",3,1,0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=RATE(48,-200,8000,\"\",1,0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=RATE(48,-200,8000,3,\"\",0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=RATE(48,-200,8000,3,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // RECEIVED - "=RECEIVED()": "RECEIVED requires at least 4 arguments", - "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,1,0)": "RECEIVED allows at most 5 arguments", - "=RECEIVED(\"\",\"03/31/2016\",1000,4.5%,1)": "#VALUE!", - "=RECEIVED(\"04/01/2011\",\"\",1000,4.5%,1)": "#VALUE!", - "=RECEIVED(\"04/01/2011\",\"03/31/2016\",\"\",4.5%,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,\"\")": "#NUM!", - "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,0)": "RECEIVED requires discount > 0", - "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,5)": "invalid basis", + "=RECEIVED()": {"#VALUE!", "RECEIVED requires at least 4 arguments"}, + "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,1,0)": {"#VALUE!", "RECEIVED allows at most 5 arguments"}, + "=RECEIVED(\"\",\"03/31/2016\",1000,4.5%,1)": {"#VALUE!", "#VALUE!"}, + "=RECEIVED(\"04/01/2011\",\"\",1000,4.5%,1)": {"#VALUE!", "#VALUE!"}, + "=RECEIVED(\"04/01/2011\",\"03/31/2016\",\"\",4.5%,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,\"\")": {"#NUM!", "#NUM!"}, + "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,0)": {"#NUM!", "RECEIVED requires discount > 0"}, + "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,5)": {"#NUM!", "invalid basis"}, // RRI - "=RRI()": "RRI requires 3 arguments", - "=RRI(\"\",\"\",\"\")": "#NUM!", - "=RRI(0,10000,15000)": "RRI requires nper argument to be > 0", - "=RRI(10,0,15000)": "RRI requires pv argument to be > 0", - "=RRI(10,10000,-1)": "RRI requires fv argument to be >= 0", + "=RRI()": {"#VALUE!", "RRI requires 3 arguments"}, + "=RRI(\"\",\"\",\"\")": {"#NUM!", "#NUM!"}, + "=RRI(0,10000,15000)": {"#NUM!", "RRI requires nper argument to be > 0"}, + "=RRI(10,0,15000)": {"#NUM!", "RRI requires pv argument to be > 0"}, + "=RRI(10,10000,-1)": {"#NUM!", "RRI requires fv argument to be >= 0"}, // SLN - "=SLN()": "SLN requires 3 arguments", - "=SLN(\"\",\"\",\"\")": "#NUM!", - "=SLN(10000,1000,0)": "SLN requires life argument to be > 0", + "=SLN()": {"#VALUE!", "SLN requires 3 arguments"}, + "=SLN(\"\",\"\",\"\")": {"#NUM!", "#NUM!"}, + "=SLN(10000,1000,0)": {"#NUM!", "SLN requires life argument to be > 0"}, // SYD - "=SYD()": "SYD requires 4 arguments", - "=SYD(\"\",\"\",\"\",\"\")": "#NUM!", - "=SYD(10000,1000,0,1)": "SYD requires life argument to be > 0", - "=SYD(10000,1000,5,0)": "SYD requires per argument to be > 0", - "=SYD(10000,1000,1,5)": "#NUM!", + "=SYD()": {"#VALUE!", "SYD requires 4 arguments"}, + "=SYD(\"\",\"\",\"\",\"\")": {"#NUM!", "#NUM!"}, + "=SYD(10000,1000,0,1)": {"#NUM!", "SYD requires life argument to be > 0"}, + "=SYD(10000,1000,5,0)": {"#NUM!", "SYD requires per argument to be > 0"}, + "=SYD(10000,1000,1,5)": {"#NUM!", "#NUM!"}, // TBILLEQ - "=TBILLEQ()": "TBILLEQ requires 3 arguments", - "=TBILLEQ(\"\",\"06/30/2017\",2.5%)": "#VALUE!", - "=TBILLEQ(\"01/01/2017\",\"\",2.5%)": "#VALUE!", - "=TBILLEQ(\"01/01/2017\",\"06/30/2017\",\"\")": "#VALUE!", - "=TBILLEQ(\"01/01/2017\",\"06/30/2017\",0)": "#NUM!", - "=TBILLEQ(\"01/01/2017\",\"06/30/2018\",2.5%)": "#NUM!", - "=TBILLEQ(\"06/30/2017\",\"01/01/2017\",2.5%)": "#NUM!", + "=TBILLEQ()": {"#VALUE!", "TBILLEQ requires 3 arguments"}, + "=TBILLEQ(\"\",\"06/30/2017\",2.5%)": {"#VALUE!", "#VALUE!"}, + "=TBILLEQ(\"01/01/2017\",\"\",2.5%)": {"#VALUE!", "#VALUE!"}, + "=TBILLEQ(\"01/01/2017\",\"06/30/2017\",\"\")": {"#VALUE!", "#VALUE!"}, + "=TBILLEQ(\"01/01/2017\",\"06/30/2017\",0)": {"#NUM!", "#NUM!"}, + "=TBILLEQ(\"01/01/2017\",\"06/30/2018\",2.5%)": {"#NUM!", "#NUM!"}, + "=TBILLEQ(\"06/30/2017\",\"01/01/2017\",2.5%)": {"#NUM!", "#NUM!"}, // TBILLPRICE - "=TBILLPRICE()": "TBILLPRICE requires 3 arguments", - "=TBILLPRICE(\"\",\"06/30/2017\",2.5%)": "#VALUE!", - "=TBILLPRICE(\"01/01/2017\",\"\",2.5%)": "#VALUE!", - "=TBILLPRICE(\"01/01/2017\",\"06/30/2017\",\"\")": "#VALUE!", - "=TBILLPRICE(\"01/01/2017\",\"06/30/2017\",0)": "#NUM!", - "=TBILLPRICE(\"01/01/2017\",\"06/30/2018\",2.5%)": "#NUM!", - "=TBILLPRICE(\"06/30/2017\",\"01/01/2017\",2.5%)": "#NUM!", + "=TBILLPRICE()": {"#VALUE!", "TBILLPRICE requires 3 arguments"}, + "=TBILLPRICE(\"\",\"06/30/2017\",2.5%)": {"#VALUE!", "#VALUE!"}, + "=TBILLPRICE(\"01/01/2017\",\"\",2.5%)": {"#VALUE!", "#VALUE!"}, + "=TBILLPRICE(\"01/01/2017\",\"06/30/2017\",\"\")": {"#VALUE!", "#VALUE!"}, + "=TBILLPRICE(\"01/01/2017\",\"06/30/2017\",0)": {"#NUM!", "#NUM!"}, + "=TBILLPRICE(\"01/01/2017\",\"06/30/2018\",2.5%)": {"#NUM!", "#NUM!"}, + "=TBILLPRICE(\"06/30/2017\",\"01/01/2017\",2.5%)": {"#NUM!", "#NUM!"}, // TBILLYIELD - "=TBILLYIELD()": "TBILLYIELD requires 3 arguments", - "=TBILLYIELD(\"\",\"06/30/2017\",2.5%)": "#VALUE!", - "=TBILLYIELD(\"01/01/2017\",\"\",2.5%)": "#VALUE!", - "=TBILLYIELD(\"01/01/2017\",\"06/30/2017\",\"\")": "#VALUE!", - "=TBILLYIELD(\"01/01/2017\",\"06/30/2017\",0)": "#NUM!", - "=TBILLYIELD(\"01/01/2017\",\"06/30/2018\",2.5%)": "#NUM!", - "=TBILLYIELD(\"06/30/2017\",\"01/01/2017\",2.5%)": "#NUM!", + "=TBILLYIELD()": {"#VALUE!", "TBILLYIELD requires 3 arguments"}, + "=TBILLYIELD(\"\",\"06/30/2017\",2.5%)": {"#VALUE!", "#VALUE!"}, + "=TBILLYIELD(\"01/01/2017\",\"\",2.5%)": {"#VALUE!", "#VALUE!"}, + "=TBILLYIELD(\"01/01/2017\",\"06/30/2017\",\"\")": {"#VALUE!", "#VALUE!"}, + "=TBILLYIELD(\"01/01/2017\",\"06/30/2017\",0)": {"#NUM!", "#NUM!"}, + "=TBILLYIELD(\"01/01/2017\",\"06/30/2018\",2.5%)": {"#NUM!", "#NUM!"}, + "=TBILLYIELD(\"06/30/2017\",\"01/01/2017\",2.5%)": {"#NUM!", "#NUM!"}, // VDB - "=VDB()": "VDB requires 5 or 7 arguments", - "=VDB(-1,1000,5,0,1)": "VDB requires cost >= 0", - "=VDB(10000,-1,5,0,1)": "VDB requires salvage >= 0", - "=VDB(10000,1000,0,0,1)": "VDB requires life > 0", - "=VDB(10000,1000,5,-1,1)": "VDB requires start_period > 0", - "=VDB(10000,1000,5,2,1)": "VDB requires start_period <= end_period", - "=VDB(10000,1000,5,0,6)": "VDB requires end_period <= life", - "=VDB(10000,1000,5,0,1,-0.2)": "VDB requires factor >= 0", - "=VDB(\"\",1000,5,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=VDB(10000,\"\",5,0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=VDB(10000,1000,\"\",0,1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=VDB(10000,1000,5,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=VDB(10000,1000,5,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=VDB(10000,1000,5,0,1,\"\")": "#NUM!", - "=VDB(10000,1000,5,0,1,0.2,\"\")": "#NUM!", + "=VDB()": {"#VALUE!", "VDB requires 5 or 7 arguments"}, + "=VDB(-1,1000,5,0,1)": {"#NUM!", "VDB requires cost >= 0"}, + "=VDB(10000,-1,5,0,1)": {"#NUM!", "VDB requires salvage >= 0"}, + "=VDB(10000,1000,0,0,1)": {"#NUM!", "VDB requires life > 0"}, + "=VDB(10000,1000,5,-1,1)": {"#NUM!", "VDB requires start_period > 0"}, + "=VDB(10000,1000,5,2,1)": {"#NUM!", "VDB requires start_period <= end_period"}, + "=VDB(10000,1000,5,0,6)": {"#NUM!", "VDB requires end_period <= life"}, + "=VDB(10000,1000,5,0,1,-0.2)": {"#VALUE!", "VDB requires factor >= 0"}, + "=VDB(\"\",1000,5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=VDB(10000,\"\",5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=VDB(10000,1000,\"\",0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=VDB(10000,1000,5,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=VDB(10000,1000,5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=VDB(10000,1000,5,0,1,\"\")": {"#NUM!", "#NUM!"}, + "=VDB(10000,1000,5,0,1,0.2,\"\")": {"#NUM!", "#NUM!"}, // YIELD - "=YIELD()": "YIELD requires 6 or 7 arguments", - "=YIELD(\"\",\"06/30/2015\",10%,101,100,4)": "#VALUE!", - "=YIELD(\"01/01/2010\",\"\",10%,101,100,4)": "#VALUE!", - "=YIELD(\"01/01/2010\",\"06/30/2015\",\"\",101,100,4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,\"\",100,4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,\"\",4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4,\"\")": "#NUM!", - "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,3)": "#NUM!", - "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4,5)": "invalid basis", - "=YIELD(\"01/01/2010\",\"06/30/2015\",-1,101,100,4)": "PRICE requires rate >= 0", - "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,0,100,4)": "PRICE requires pr > 0", - "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,-1,4)": "PRICE requires redemption >= 0", + "=YIELD()": {"#VALUE!", "YIELD requires 6 or 7 arguments"}, + "=YIELD(\"\",\"06/30/2015\",10%,101,100,4)": {"#VALUE!", "#VALUE!"}, + "=YIELD(\"01/01/2010\",\"\",10%,101,100,4)": {"#VALUE!", "#VALUE!"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",\"\",101,100,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,\"\",100,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,\"\",4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4,\"\")": {"#NUM!", "#NUM!"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,3)": {"#NUM!", "#NUM!"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4,5)": {"#NUM!", "invalid basis"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",-1,101,100,4)": {"#NUM!", "PRICE requires rate >= 0"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,0,100,4)": {"#NUM!", "PRICE requires pr > 0"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,-1,4)": {"#NUM!", "PRICE requires redemption >= 0"}, // YIELDDISC - "=YIELDDISC()": "YIELDDISC requires 4 or 5 arguments", - "=YIELDDISC(\"\",\"06/30/2017\",97,100,0)": "#VALUE!", - "=YIELDDISC(\"01/01/2017\",\"\",97,100,0)": "#VALUE!", - "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",\"\",100,0)": "#VALUE!", - "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,\"\",0)": "#VALUE!", - "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,\"\")": "#NUM!", - "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",0,100)": "YIELDDISC requires pr > 0", - "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,0)": "YIELDDISC requires redemption > 0", - "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,5)": "invalid basis", + "=YIELDDISC()": {"#VALUE!", "YIELDDISC requires 4 or 5 arguments"}, + "=YIELDDISC(\"\",\"06/30/2017\",97,100,0)": {"#VALUE!", "#VALUE!"}, + "=YIELDDISC(\"01/01/2017\",\"\",97,100,0)": {"#VALUE!", "#VALUE!"}, + "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",\"\",100,0)": {"#VALUE!", "#VALUE!"}, + "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,\"\",0)": {"#VALUE!", "#VALUE!"}, + "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,\"\")": {"#NUM!", "#NUM!"}, + "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",0,100)": {"#NUM!", "YIELDDISC requires pr > 0"}, + "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,0)": {"#NUM!", "YIELDDISC requires redemption > 0"}, + "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,5)": {"#NUM!", "invalid basis"}, // YIELDMAT - "=YIELDMAT()": "YIELDMAT requires 5 or 6 arguments", - "=YIELDMAT(\"\",\"06/30/2018\",\"06/01/2014\",5.5%,101,0)": "#VALUE!", - "=YIELDMAT(\"01/01/2017\",\"\",\"06/01/2014\",5.5%,101,0)": "#VALUE!", - "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"\",5.5%,101,0)": "#VALUE!", - "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",\"\",101,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5,5.5%,\"\")": "#NUM!", - "=YIELDMAT(\"06/01/2014\",\"06/30/2018\",\"01/01/2017\",5.5%,101,0)": "YIELDMAT requires settlement > issue", - "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",-1,101,0)": "YIELDMAT requires rate >= 0", - "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",1,0,0)": "YIELDMAT requires pr > 0", - "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101,5)": "invalid basis", + "=YIELDMAT()": {"#VALUE!", "YIELDMAT requires 5 or 6 arguments"}, + "=YIELDMAT(\"\",\"06/30/2018\",\"06/01/2014\",5.5%,101,0)": {"#VALUE!", "#VALUE!"}, + "=YIELDMAT(\"01/01/2017\",\"\",\"06/01/2014\",5.5%,101,0)": {"#VALUE!", "#VALUE!"}, + "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"\",5.5%,101,0)": {"#VALUE!", "#VALUE!"}, + "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",\"\",101,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5,5.5%,\"\")": {"#NUM!", "#NUM!"}, + "=YIELDMAT(\"06/01/2014\",\"06/30/2018\",\"01/01/2017\",5.5%,101,0)": {"#NUM!", "YIELDMAT requires settlement > issue"}, + "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",-1,101,0)": {"#NUM!", "YIELDMAT requires rate >= 0"}, + "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",1,0,0)": {"#NUM!", "YIELDMAT requires pr > 0"}, + "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101,5)": {"#NUM!", "invalid basis"}, } for formula, expected := range mathCalcError { f := prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } referenceCalc := map[string]string{ @@ -4393,18 +4394,18 @@ func TestCalcCellValue(t *testing.T) { assert.Equal(t, expected, result, formula) } - referenceCalcError := map[string]string{ + referenceCalcError := map[string][]string{ // MDETERM - "=MDETERM(A1:B3)": "#VALUE!", + "=MDETERM(A1:B3)": {"#VALUE!", "#VALUE!"}, // SUM - "=1+SUM(SUM(A1+A2/A4)*(2-3),2)": "#DIV/0!", + "=1+SUM(SUM(A1+A2/A4)*(2-3),2)": {"#VALUE!", "#DIV/0!"}, } for formula, expected := range referenceCalcError { f := prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } volatileFuncs := []string{ @@ -4455,25 +4456,25 @@ func TestCalcWithDefinedName(t *testing.T) { // DefinedName with scope WorkSheet takes precedence over DefinedName with scope Workbook, so we should get B1 value assert.Equal(t, "B1_as_string", result, "=defined_name1") - assert.NoError(t, f.SetCellFormula("Sheet1", "D1", `=CONCATENATE("<",defined_name1,">")`)) + assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=CONCATENATE(\"<\",defined_name1,\">\")")) result, err = f.CalcCellValue("Sheet1", "D1") assert.NoError(t, err) assert.Equal(t, "", result, "=defined_name1") // comparing numeric values - assert.NoError(t, f.SetCellFormula("Sheet1", "D1", `=123=defined_name2`)) + assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=123=defined_name2")) result, err = f.CalcCellValue("Sheet1", "D1") assert.NoError(t, err) assert.Equal(t, "TRUE", result, "=123=defined_name2") // comparing text values - assert.NoError(t, f.SetCellFormula("Sheet1", "D1", `="B1_as_string"=defined_name1`)) + assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=\"B1_as_string\"=defined_name1")) result, err = f.CalcCellValue("Sheet1", "D1") assert.NoError(t, err) - assert.Equal(t, "TRUE", result, `="B1_as_string"=defined_name1`) + assert.Equal(t, "TRUE", result, "=\"B1_as_string\"=defined_name1") // comparing text values - assert.NoError(t, f.SetCellFormula("Sheet1", "D1", `=IF("B1_as_string"=defined_name1,"YES","NO")`)) + assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=IF(\"B1_as_string\"=defined_name1,\"YES\",\"NO\")")) result, err = f.CalcCellValue("Sheet1", "D1") assert.NoError(t, err) assert.Equal(t, "YES", result, `=IF("B1_as_string"=defined_name1,"YES","NO")`) @@ -4594,14 +4595,14 @@ func TestCalcVLOOKUP(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=VLOOKUP(INT(1),C3:C3,1,FALSE)": "VLOOKUP no result found", + calcError := map[string][]string{ + "=VLOOKUP(INT(1),C3:C3,1,FALSE)": {"#N/A", "VLOOKUP no result found"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "F4", formula)) result, err := f.CalcCellValue("Sheet1", "F4") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -4706,19 +4707,19 @@ func TestCalcCOVAR(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=COVAR()": "COVAR requires 2 arguments", - "=COVAR(A2:A9,B3:B3)": "#N/A", - "=COVARIANCE.P()": "COVARIANCE.P requires 2 arguments", - "=COVARIANCE.P(A2:A9,B3:B3)": "#N/A", - "=COVARIANCE.S()": "COVARIANCE.S requires 2 arguments", - "=COVARIANCE.S(A2:A9,B3:B3)": "#N/A", + calcError := map[string][]string{ + "=COVAR()": {"#VALUE!", "COVAR requires 2 arguments"}, + "=COVAR(A2:A9,B3:B3)": {"#N/A", "#N/A"}, + "=COVARIANCE.P()": {"#VALUE!", "COVARIANCE.P requires 2 arguments"}, + "=COVARIANCE.P(A2:A9,B3:B3)": {"#N/A", "#N/A"}, + "=COVARIANCE.S()": {"#VALUE!", "COVARIANCE.S requires 2 arguments"}, + "=COVARIANCE.S(A2:A9,B3:B3)": {"#N/A", "#N/A"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -4769,47 +4770,47 @@ func TestCalcDatabase(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=DAVERAGE()": "DAVERAGE requires 3 arguments", - "=DAVERAGE(A4:E10,\"x\",A1:F3)": "#VALUE!", - "=DAVERAGE(A4:E10,\"Tree\",A1:F3)": "#DIV/0!", - "=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!", - "=DGET()": "DGET requires 3 arguments", - "=DGET(A4:E5,\"Profit\",A1:F3)": "#VALUE!", - "=DGET(A4:E10,\"Profit\",A1:F3)": "#NUM!", - "=DMAX()": "DMAX requires 3 arguments", - "=DMAX(A4:E10,\"x\",A1:F3)": "#VALUE!", - "=DMIN()": "DMIN requires 3 arguments", - "=DMIN(A4:E10,\"x\",A1:F3)": "#VALUE!", - "=DPRODUCT()": "DPRODUCT requires 3 arguments", - "=DPRODUCT(A4:E10,\"x\",A1:F3)": "#VALUE!", - "=DSTDEV()": "DSTDEV requires 3 arguments", - "=DSTDEV(A4:E10,\"x\",A1:F3)": "#VALUE!", - "=DSTDEVP()": "DSTDEVP requires 3 arguments", - "=DSTDEVP(A4:E10,\"x\",A1:F3)": "#VALUE!", - "=DSUM()": "DSUM requires 3 arguments", - "=DSUM(A4:E10,\"x\",A1:F3)": "#VALUE!", - "=DVAR()": "DVAR requires 3 arguments", - "=DVAR(A4:E10,\"x\",A1:F3)": "#VALUE!", - "=DVARP()": "DVARP requires 3 arguments", - "=DVARP(A4:E10,\"x\",A1:F3)": "#VALUE!", + calcError := map[string][]string{ + "=DAVERAGE()": {"#VALUE!", "DAVERAGE requires 3 arguments"}, + "=DAVERAGE(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"}, + "=DAVERAGE(A4:E10,\"Tree\",A1:F3)": {"#DIV/0!", "#DIV/0!"}, + "=DCOUNT()": {"#VALUE!", "DCOUNT requires at least 2 arguments"}, + "=DCOUNT(A4:E10,\"Age\",A1:F2,\"\")": {"#VALUE!", "DCOUNT allows at most 3 arguments"}, + "=DCOUNT(A4,\"Age\",A1:F2)": {"#VALUE!", "#VALUE!"}, + "=DCOUNT(A4:E10,NA(),A1:F2)": {"#VALUE!", "#VALUE!"}, + "=DCOUNT(A4:E4,,A1:F2)": {"#VALUE!", "#VALUE!"}, + "=DCOUNT(A4:E10,\"x\",A2:F3)": {"#VALUE!", "#VALUE!"}, + "=DCOUNTA()": {"#VALUE!", "DCOUNTA requires at least 2 arguments"}, + "=DCOUNTA(A4:E10,\"Age\",A1:F2,\"\")": {"#VALUE!", "DCOUNTA allows at most 3 arguments"}, + "=DCOUNTA(A4,\"Age\",A1:F2)": {"#VALUE!", "#VALUE!"}, + "=DCOUNTA(A4:E10,NA(),A1:F2)": {"#VALUE!", "#VALUE!"}, + "=DCOUNTA(A4:E4,,A1:F2)": {"#VALUE!", "#VALUE!"}, + "=DCOUNTA(A4:E10,\"x\",A2:F3)": {"#VALUE!", "#VALUE!"}, + "=DGET()": {"#VALUE!", "DGET requires 3 arguments"}, + "=DGET(A4:E5,\"Profit\",A1:F3)": {"#VALUE!", "#VALUE!"}, + "=DGET(A4:E10,\"Profit\",A1:F3)": {"#NUM!", "#NUM!"}, + "=DMAX()": {"#VALUE!", "DMAX requires 3 arguments"}, + "=DMAX(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"}, + "=DMIN()": {"#VALUE!", "DMIN requires 3 arguments"}, + "=DMIN(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"}, + "=DPRODUCT()": {"#VALUE!", "DPRODUCT requires 3 arguments"}, + "=DPRODUCT(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"}, + "=DSTDEV()": {"#VALUE!", "DSTDEV requires 3 arguments"}, + "=DSTDEV(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"}, + "=DSTDEVP()": {"#VALUE!", "DSTDEVP requires 3 arguments"}, + "=DSTDEVP(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"}, + "=DSUM()": {"#VALUE!", "DSUM requires 3 arguments"}, + "=DSUM(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"}, + "=DVAR()": {"#VALUE!", "DVAR requires 3 arguments"}, + "=DVAR(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"}, + "=DVARP()": {"#VALUE!", "DVARP requires 3 arguments"}, + "=DVARP(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#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) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -4867,38 +4868,38 @@ func TestCalcGROWTHandTREND(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=GROWTH()": "GROWTH requires at least 1 argument", - "=GROWTH(B2:B5,A2:A5,A8:A10,TRUE,0)": "GROWTH allows at most 4 arguments", - "=GROWTH(A1:B1,A2:A5,A8:A10,TRUE)": "#VALUE!", - "=GROWTH(B2:B5,A1:B1,A8:A10,TRUE)": "#VALUE!", - "=GROWTH(B2:B5,A2:A5,A1:B1,TRUE)": "#VALUE!", - "=GROWTH(B2:B5,A2:A5,A8:A10,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=GROWTH(A2:B3,A4:B4)": "#REF!", - "=GROWTH(A4:B4,A2:A2)": "#REF!", - "=GROWTH(A2:A2,A4:A5)": "#REF!", - "=GROWTH(C1:C1,A2:A3)": "#VALUE!", - "=GROWTH(D1:D1,A2:A3)": "#NUM!", - "=GROWTH(A2:A3,C1:C1)": "#VALUE!", - "=TREND()": "TREND requires at least 1 argument", - "=TREND(B2:B5,A2:A5,A8:A10,TRUE,0)": "TREND allows at most 4 arguments", - "=TREND(A1:B1,A2:A5,A8:A10,TRUE)": "#VALUE!", - "=TREND(B2:B5,A1:B1,A8:A10,TRUE)": "#VALUE!", - "=TREND(B2:B5,A2:A5,A1:B1,TRUE)": "#VALUE!", - "=TREND(B2:B5,A2:A5,A8:A10,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", - "=TREND(A2:B3,A4:B4)": "#REF!", - "=TREND(A4:B4,A2:A2)": "#REF!", - "=TREND(A2:A2,A4:A5)": "#REF!", - "=TREND(C1:C1,A2:A3)": "#VALUE!", - "=TREND(D1:D1,A2:A3)": "#REF!", - "=TREND(A2:A3,C1:C1)": "#VALUE!", - "=TREND(C1:C1,C1:C1)": "#VALUE!", + calcError := map[string][]string{ + "=GROWTH()": {"#VALUE!", "GROWTH requires at least 1 argument"}, + "=GROWTH(B2:B5,A2:A5,A8:A10,TRUE,0)": {"#VALUE!", "GROWTH allows at most 4 arguments"}, + "=GROWTH(A1:B1,A2:A5,A8:A10,TRUE)": {"#VALUE!", "#VALUE!"}, + "=GROWTH(B2:B5,A1:B1,A8:A10,TRUE)": {"#VALUE!", "#VALUE!"}, + "=GROWTH(B2:B5,A2:A5,A1:B1,TRUE)": {"#VALUE!", "#VALUE!"}, + "=GROWTH(B2:B5,A2:A5,A8:A10,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=GROWTH(A2:B3,A4:B4)": {"#REF!", "#REF!"}, + "=GROWTH(A4:B4,A2:A2)": {"#REF!", "#REF!"}, + "=GROWTH(A2:A2,A4:A5)": {"#REF!", "#REF!"}, + "=GROWTH(C1:C1,A2:A3)": {"#VALUE!", "#VALUE!"}, + "=GROWTH(D1:D1,A2:A3)": {"#NUM!", "#NUM!"}, + "=GROWTH(A2:A3,C1:C1)": {"#VALUE!", "#VALUE!"}, + "=TREND()": {"#VALUE!", "TREND requires at least 1 argument"}, + "=TREND(B2:B5,A2:A5,A8:A10,TRUE,0)": {"#VALUE!", "TREND allows at most 4 arguments"}, + "=TREND(A1:B1,A2:A5,A8:A10,TRUE)": {"#VALUE!", "#VALUE!"}, + "=TREND(B2:B5,A1:B1,A8:A10,TRUE)": {"#VALUE!", "#VALUE!"}, + "=TREND(B2:B5,A2:A5,A1:B1,TRUE)": {"#VALUE!", "#VALUE!"}, + "=TREND(B2:B5,A2:A5,A8:A10,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=TREND(A2:B3,A4:B4)": {"#REF!", "#REF!"}, + "=TREND(A4:B4,A2:A2)": {"#REF!", "#REF!"}, + "=TREND(A2:A2,A4:A5)": {"#REF!", "#REF!"}, + "=TREND(C1:C1,A2:A3)": {"#VALUE!", "#VALUE!"}, + "=TREND(D1:D1,A2:A3)": {"#REF!", "#REF!"}, + "=TREND(A2:A3,C1:C1)": {"#VALUE!", "#VALUE!"}, + "=TREND(C1:C1,C1:C1)": {"#VALUE!", "#VALUE!"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -4929,14 +4930,14 @@ func TestCalcHLOOKUP(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=HLOOKUP(INT(1),A3:A3,1,FALSE)": "HLOOKUP no result found", + calcError := map[string][]string{ + "=HLOOKUP(INT(1),A3:A3,1,FALSE)": {"#N/A", "HLOOKUP no result found"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "B10", formula)) result, err := f.CalcCellValue("Sheet1", "B10") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -4966,23 +4967,23 @@ func TestCalcCHITESTandCHISQdotTEST(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=CHITEST()": "CHITEST requires 2 arguments", - "=CHITEST(B3:C5,F3:F4)": "#N/A", - "=CHITEST(B3:B3,F3:F3)": "#N/A", - "=CHITEST(F3:F5,B4:B6)": "#NUM!", - "=CHITEST(F3:F5,C4:C6)": "#DIV/0!", - "=CHISQ.TEST()": "CHISQ.TEST requires 2 arguments", - "=CHISQ.TEST(B3:C5,F3:F4)": "#N/A", - "=CHISQ.TEST(B3:B3,F3:F3)": "#N/A", - "=CHISQ.TEST(F3:F5,B4:B6)": "#NUM!", - "=CHISQ.TEST(F3:F5,C4:C6)": "#DIV/0!", + calcError := map[string][]string{ + "=CHITEST()": {"#VALUE!", "CHITEST requires 2 arguments"}, + "=CHITEST(B3:C5,F3:F4)": {"#N/A", "#N/A"}, + "=CHITEST(B3:B3,F3:F3)": {"#N/A", "#N/A"}, + "=CHITEST(F3:F5,B4:B6)": {"#NUM!", "#NUM!"}, + "=CHITEST(F3:F5,C4:C6)": {"#DIV/0!", "#DIV/0!"}, + "=CHISQ.TEST()": {"#VALUE!", "CHISQ.TEST requires 2 arguments"}, + "=CHISQ.TEST(B3:C5,F3:F4)": {"#N/A", "#N/A"}, + "=CHISQ.TEST(B3:B3,F3:F3)": {"#N/A", "#N/A"}, + "=CHISQ.TEST(F3:F5,B4:B6)": {"#NUM!", "#NUM!"}, + "=CHISQ.TEST(F3:F5,C4:C6)": {"#DIV/0!", "#DIV/0!"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "I1", formula)) result, err := f.CalcCellValue("Sheet1", "I1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -5011,23 +5012,23 @@ func TestCalcFTEST(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=FTEST()": "FTEST requires 2 arguments", - "=FTEST(A2:A2,B2:B2)": "#DIV/0!", - "=FTEST(A12:A14,B2:B4)": "#DIV/0!", - "=FTEST(A2:A4,B2:B2)": "#DIV/0!", - "=FTEST(A2:A4,B12:B14)": "#DIV/0!", - "=F.TEST()": "F.TEST requires 2 arguments", - "=F.TEST(A2:A2,B2:B2)": "#DIV/0!", - "=F.TEST(A12:A14,B2:B4)": "#DIV/0!", - "=F.TEST(A2:A4,B2:B2)": "#DIV/0!", - "=F.TEST(A2:A4,B12:B14)": "#DIV/0!", + calcError := map[string][]string{ + "=FTEST()": {"#VALUE!", "FTEST requires 2 arguments"}, + "=FTEST(A2:A2,B2:B2)": {"#DIV/0!", "#DIV/0!"}, + "=FTEST(A12:A14,B2:B4)": {"#DIV/0!", "#DIV/0!"}, + "=FTEST(A2:A4,B2:B2)": {"#DIV/0!", "#DIV/0!"}, + "=FTEST(A2:A4,B12:B14)": {"#DIV/0!", "#DIV/0!"}, + "=F.TEST()": {"#VALUE!", "F.TEST requires 2 arguments"}, + "=F.TEST(A2:A2,B2:B2)": {"#DIV/0!", "#DIV/0!"}, + "=F.TEST(A12:A14,B2:B4)": {"#DIV/0!", "#DIV/0!"}, + "=F.TEST(A2:A4,B2:B2)": {"#DIV/0!", "#DIV/0!"}, + "=F.TEST(A2:A4,B12:B14)": {"#DIV/0!", "#DIV/0!"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -5045,17 +5046,17 @@ func TestCalcIRR(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=IRR()": "IRR requires at least 1 argument", - "=IRR(0,0,0)": "IRR allows at most 2 arguments", - "=IRR(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=IRR(A2:A3)": "#NUM!", + calcError := map[string][]string{ + "=IRR()": {"#VALUE!", "IRR requires at least 1 argument"}, + "=IRR(0,0,0)": {"#VALUE!", "IRR allows at most 2 arguments"}, + "=IRR(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=IRR(A2:A3)": {"#NUM!", "#NUM!"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "B1", formula)) result, err := f.CalcCellValue("Sheet1", "B1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -5095,17 +5096,17 @@ func TestCalcMIRR(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=MIRR()": "MIRR requires 3 arguments", - "=MIRR(A1:A5,\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MIRR(A1:A5,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MIRR(B1:B5,0,0)": "#DIV/0!", + calcError := map[string][]string{ + "=MIRR()": {"#VALUE!", "MIRR requires 3 arguments"}, + "=MIRR(A1:A5,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=MIRR(A1:A5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=MIRR(B1:B5,0,0)": {"#DIV/0!", "#DIV/0!"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "B1", formula)) result, err := f.CalcCellValue("Sheet1", "B1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -5138,20 +5139,20 @@ func TestCalcSUMIFSAndAVERAGEIFS(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=AVERAGEIFS()": "AVERAGEIFS requires at least 3 arguments", - "=AVERAGEIFS(H1,\"\")": "AVERAGEIFS requires at least 3 arguments", - "=AVERAGEIFS(H1,\"\",TRUE,1)": "#N/A", - "=AVERAGEIFS(H1,\"\",TRUE)": "AVERAGEIF divide by zero", - "=SUMIFS()": "SUMIFS requires at least 3 arguments", - "=SUMIFS(D2:D13,A2:A13,1,B2:B13)": "#N/A", - "=SUMIFS(D20:D23,A2:A13,\">2\",C2:C13,\"Jeff\")": "#VALUE!", + calcError := map[string][]string{ + "=AVERAGEIFS()": {"#VALUE!", "AVERAGEIFS requires at least 3 arguments"}, + "=AVERAGEIFS(H1,\"\")": {"#VALUE!", "AVERAGEIFS requires at least 3 arguments"}, + "=AVERAGEIFS(H1,\"\",TRUE,1)": {"#N/A", "#N/A"}, + "=AVERAGEIFS(H1,\"\",TRUE)": {"#DIV/0!", "AVERAGEIF divide by zero"}, + "=SUMIFS()": {"#VALUE!", "SUMIFS requires at least 3 arguments"}, + "=SUMIFS(D2:D13,A2:A13,1,B2:B13)": {"#N/A", "#N/A"}, + "=SUMIFS(D20:D23,A2:A13,\">2\",C2:C13,\"Jeff\")": {"#VALUE!", "#VALUE!"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula)) result, err := f.CalcCellValue("Sheet1", "E1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -5176,20 +5177,20 @@ func TestCalcXIRR(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=XIRR()": "XIRR requires 2 or 3 arguments", - "=XIRR(A1:A4,B1:B4,-1)": "XIRR requires guess > -1", - "=XIRR(\"\",B1:B4)": "#NUM!", - "=XIRR(A1:A4,\"\")": "#NUM!", - "=XIRR(A1:A4,B1:B4,\"\")": "#NUM!", - "=XIRR(A2:A6,B2:B6)": "#NUM!", - "=XIRR(A2:A7,B2:B7)": "#NUM!", + calcError := map[string][]string{ + "=XIRR()": {"#VALUE!", "XIRR requires 2 or 3 arguments"}, + "=XIRR(A1:A4,B1:B4,-1)": {"#VALUE!", "XIRR requires guess > -1"}, + "=XIRR(\"\",B1:B4)": {"#NUM!", "#NUM!"}, + "=XIRR(A1:A4,\"\")": {"#NUM!", "#NUM!"}, + "=XIRR(A1:A4,B1:B4,\"\")": {"#NUM!", "#NUM!"}, + "=XIRR(A2:A6,B2:B6)": {"#NUM!", "#NUM!"}, + "=XIRR(A2:A7,B2:B7)": {"#NUM!", "#NUM!"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -5230,25 +5231,25 @@ func TestCalcXLOOKUP(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=XLOOKUP()": "XLOOKUP requires at least 3 arguments", - "=XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,2,1)": "XLOOKUP allows at most 6 arguments", - "=XLOOKUP($C3,$C5,$C6,NA(),0,2)": "#N/A", - "=XLOOKUP(\"?\",B2:B9,C2:C9,NA(),2)": "#N/A", - "=XLOOKUP($C3,$C4:$D5,$C6:$C17,NA(),0,2)": "#VALUE!", - "=XLOOKUP($C3,$C5:$C5,$C6:$G17,NA(),0,-2)": "#VALUE!", - "=XLOOKUP($C3,$C5:$G5,$C6:$F7,NA(),0,2)": "#VALUE!", - "=XLOOKUP(D2,$B6:$B17,$C6:$G16,NA(),0,2)": "#VALUE!", - "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),3,2)": "#VALUE!", - "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),0,0)": "#VALUE!", - "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + calcError := map[string][]string{ + "=XLOOKUP()": {"#VALUE!", "XLOOKUP requires at least 3 arguments"}, + "=XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,2,1)": {"#VALUE!", "XLOOKUP allows at most 6 arguments"}, + "=XLOOKUP($C3,$C5,$C6,NA(),0,2)": {"#N/A", "#N/A"}, + "=XLOOKUP(\"?\",B2:B9,C2:C9,NA(),2)": {"#N/A", "#N/A"}, + "=XLOOKUP($C3,$C4:$D5,$C6:$C17,NA(),0,2)": {"#VALUE!", "#VALUE!"}, + "=XLOOKUP($C3,$C5:$C5,$C6:$G17,NA(),0,-2)": {"#VALUE!", "#VALUE!"}, + "=XLOOKUP($C3,$C5:$G5,$C6:$F7,NA(),0,2)": {"#VALUE!", "#VALUE!"}, + "=XLOOKUP(D2,$B6:$B17,$C6:$G16,NA(),0,2)": {"#VALUE!", "#VALUE!"}, + "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),3,2)": {"#VALUE!", "#VALUE!"}, + "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),0,0)": {"#VALUE!", "#VALUE!"}, + "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula)) result, err := f.CalcCellValue("Sheet1", "D3") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } cellData = [][]interface{}{ @@ -5289,15 +5290,15 @@ func TestCalcXLOOKUP(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError = map[string]string{ + calcError = map[string][]string{ // Test match mode with exact match - "=XLOOKUP(\"*p*\",B2:B9,C2:C9,NA(),0)": "#N/A", + "=XLOOKUP(\"*p*\",B2:B9,C2:C9,NA(),0)": {"#N/A", "#N/A"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula)) result, err := f.CalcCellValue("Sheet1", "D3") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -5324,22 +5325,22 @@ func TestCalcXNPV(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=XNPV()": "XNPV requires 3 arguments", - "=XNPV(\"\",B2:B7,A2:A7)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=XNPV(0,B2:B7,A2:A7)": "XNPV requires rate > 0", - "=XNPV(B1,\"\",A2:A7)": "#NUM!", - "=XNPV(B1,B2:B7,\"\")": "#NUM!", - "=XNPV(B1,B2:B7,C2:C7)": "#NUM!", - "=XNPV(B1,B2,A2)": "#NUM!", - "=XNPV(B1,B2:B3,A2:A5)": "#NUM!", - "=XNPV(B1,B2:B3,A9:A10)": "#VALUE!", + calcError := map[string][]string{ + "=XNPV()": {"#VALUE!", "XNPV requires 3 arguments"}, + "=XNPV(\"\",B2:B7,A2:A7)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=XNPV(0,B2:B7,A2:A7)": {"#VALUE!", "XNPV requires rate > 0"}, + "=XNPV(B1,\"\",A2:A7)": {"#NUM!", "#NUM!"}, + "=XNPV(B1,B2:B7,\"\")": {"#NUM!", "#NUM!"}, + "=XNPV(B1,B2:B7,C2:C7)": {"#NUM!", "#NUM!"}, + "=XNPV(B1,B2,A2)": {"#NUM!", "#NUM!"}, + "=XNPV(B1,B2:B3,A2:A5)": {"#NUM!", "#NUM!"}, + "=XNPV(B1,B2:B3,A9:A10)": {"#VALUE!", "#VALUE!"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -5380,7 +5381,7 @@ func TestCalcMATCH(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula)) result, err := f.CalcCellValue("Sheet1", "E1") assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected, result, formula) } assert.Equal(t, newErrorFormulaArg(formulaErrorNA, formulaErrorNA), calcMatch(2, nil, []formulaArg{})) } @@ -5423,22 +5424,22 @@ func TestCalcMODE(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=MODE()": "MODE requires at least 1 argument", - "=MODE(0,\"\")": "#VALUE!", - "=MODE(D1:D3)": "#N/A", - "=MODE.MULT()": "MODE.MULT requires at least 1 argument", - "=MODE.MULT(0,\"\")": "#VALUE!", - "=MODE.MULT(D1:D3)": "#N/A", - "=MODE.SNGL()": "MODE.SNGL requires at least 1 argument", - "=MODE.SNGL(0,\"\")": "#VALUE!", - "=MODE.SNGL(D1:D3)": "#N/A", + calcError := map[string][]string{ + "=MODE()": {"#VALUE!", "MODE requires at least 1 argument"}, + "=MODE(0,\"\")": {"#VALUE!", "#VALUE!"}, + "=MODE(D1:D3)": {"#N/A", "#N/A"}, + "=MODE.MULT()": {"#VALUE!", "MODE.MULT requires at least 1 argument"}, + "=MODE.MULT(0,\"\")": {"#VALUE!", "#VALUE!"}, + "=MODE.MULT(D1:D3)": {"#N/A", "#N/A"}, + "=MODE.SNGL()": {"#VALUE!", "MODE.SNGL requires at least 1 argument"}, + "=MODE.SNGL(0,\"\")": {"#VALUE!", "#VALUE!"}, + "=MODE.SNGL(D1:D3)": {"#N/A", "#N/A"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -5582,16 +5583,16 @@ func TestCalcSTEY(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=STEYX()": "STEYX requires 2 arguments", - "=STEYX(B2:B11,A1:A9)": "#N/A", - "=STEYX(B2,A2)": "#DIV/0!", + calcError := map[string][]string{ + "=STEYX()": {"#VALUE!", "STEYX requires 2 arguments"}, + "=STEYX(B2:B11,A1:A9)": {"#N/A", "#N/A"}, + "=STEYX(B2,A2)": {"#DIV/0!", "#DIV/0!"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -5631,37 +5632,37 @@ func TestCalcTTEST(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=TTEST()": "TTEST requires 4 arguments", - "=TTEST(\"\",B1:B12,1,1)": "#NUM!", - "=TTEST(A1:A12,\"\",1,1)": "#NUM!", - "=TTEST(A1:A12,B1:B12,\"\",1)": "#VALUE!", - "=TTEST(A1:A12,B1:B12,1,\"\")": "#VALUE!", - "=TTEST(A1:A12,B1:B12,0,1)": "#NUM!", - "=TTEST(A1:A12,B1:B12,1,0)": "#NUM!", - "=TTEST(A1:A2,B1:B1,1,1)": "#N/A", - "=TTEST(A13:A14,B13:B14,1,1)": "#NUM!", - "=TTEST(A12:A13,B12:B13,1,1)": "#DIV/0!", - "=TTEST(A13:A14,B13:B14,1,2)": "#NUM!", - "=TTEST(D1:D4,E1:E4,1,3)": "#NUM!", - "=T.TEST()": "T.TEST requires 4 arguments", - "=T.TEST(\"\",B1:B12,1,1)": "#NUM!", - "=T.TEST(A1:A12,\"\",1,1)": "#NUM!", - "=T.TEST(A1:A12,B1:B12,\"\",1)": "#VALUE!", - "=T.TEST(A1:A12,B1:B12,1,\"\")": "#VALUE!", - "=T.TEST(A1:A12,B1:B12,0,1)": "#NUM!", - "=T.TEST(A1:A12,B1:B12,1,0)": "#NUM!", - "=T.TEST(A1:A2,B1:B1,1,1)": "#N/A", - "=T.TEST(A13:A14,B13:B14,1,1)": "#NUM!", - "=T.TEST(A12:A13,B12:B13,1,1)": "#DIV/0!", - "=T.TEST(A13:A14,B13:B14,1,2)": "#NUM!", - "=T.TEST(D1:D4,E1:E4,1,3)": "#NUM!", + calcError := map[string][]string{ + "=TTEST()": {"#VALUE!", "TTEST requires 4 arguments"}, + "=TTEST(\"\",B1:B12,1,1)": {"#NUM!", "#NUM!"}, + "=TTEST(A1:A12,\"\",1,1)": {"#NUM!", "#NUM!"}, + "=TTEST(A1:A12,B1:B12,\"\",1)": {"#VALUE!", "#VALUE!"}, + "=TTEST(A1:A12,B1:B12,1,\"\")": {"#VALUE!", "#VALUE!"}, + "=TTEST(A1:A12,B1:B12,0,1)": {"#NUM!", "#NUM!"}, + "=TTEST(A1:A12,B1:B12,1,0)": {"#NUM!", "#NUM!"}, + "=TTEST(A1:A2,B1:B1,1,1)": {"#N/A", "#N/A"}, + "=TTEST(A13:A14,B13:B14,1,1)": {"#NUM!", "#NUM!"}, + "=TTEST(A12:A13,B12:B13,1,1)": {"#DIV/0!", "#DIV/0!"}, + "=TTEST(A13:A14,B13:B14,1,2)": {"#NUM!", "#NUM!"}, + "=TTEST(D1:D4,E1:E4,1,3)": {"#NUM!", "#NUM!"}, + "=T.TEST()": {"#VALUE!", "T.TEST requires 4 arguments"}, + "=T.TEST(\"\",B1:B12,1,1)": {"#NUM!", "#NUM!"}, + "=T.TEST(A1:A12,\"\",1,1)": {"#NUM!", "#NUM!"}, + "=T.TEST(A1:A12,B1:B12,\"\",1)": {"#VALUE!", "#VALUE!"}, + "=T.TEST(A1:A12,B1:B12,1,\"\")": {"#VALUE!", "#VALUE!"}, + "=T.TEST(A1:A12,B1:B12,0,1)": {"#NUM!", "#NUM!"}, + "=T.TEST(A1:A12,B1:B12,1,0)": {"#NUM!", "#NUM!"}, + "=T.TEST(A1:A2,B1:B1,1,1)": {"#N/A", "#N/A"}, + "=T.TEST(A13:A14,B13:B14,1,1)": {"#NUM!", "#NUM!"}, + "=T.TEST(A12:A13,B12:B13,1,1)": {"#DIV/0!", "#DIV/0!"}, + "=T.TEST(A13:A14,B13:B14,1,2)": {"#NUM!", "#NUM!"}, + "=T.TEST(D1:D4,E1:E4,1,3)": {"#NUM!", "#NUM!"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } @@ -5735,41 +5736,41 @@ func TestCalcNETWORKDAYSandWORKDAY(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } - calcError := map[string]string{ - "=NETWORKDAYS()": "NETWORKDAYS requires at least 2 arguments", - "=NETWORKDAYS(\"01/01/2020\",\"09/12/2020\",2,\"\")": "NETWORKDAYS requires at most 3 arguments", - "=NETWORKDAYS(\"\",\"09/12/2020\",2)": "#VALUE!", - "=NETWORKDAYS(\"01/01/2020\",\"\",2)": "#VALUE!", - "=NETWORKDAYS.INTL()": "NETWORKDAYS.INTL requires at least 2 arguments", - "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",4,A1:A12,\"\")": "NETWORKDAYS.INTL requires at most 4 arguments", - "=NETWORKDAYS.INTL(\"01/01/2020\",\"January 25, 100\",4)": "#VALUE!", - "=NETWORKDAYS.INTL(\"\",123,4,B1:B12)": "#VALUE!", - "=NETWORKDAYS.INTL(\"01/01/2020\",123,\"000000x\")": "#VALUE!", - "=NETWORKDAYS.INTL(\"01/01/2020\",123,\"0000002\")": "#VALUE!", - "=NETWORKDAYS.INTL(\"January 25, 100\",123)": "#VALUE!", - "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",8)": "#VALUE!", - "=NETWORKDAYS.INTL(-1,123)": "#NUM!", - "=WORKDAY()": "WORKDAY requires at least 2 arguments", - "=WORKDAY(\"01/01/2020\",123,A1:A12,\"\")": "WORKDAY requires at most 3 arguments", - "=WORKDAY(\"01/01/2020\",\"\",B1:B12)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=WORKDAY(\"\",123,B1:B12)": "#VALUE!", - "=WORKDAY(\"January 25, 100\",123)": "#VALUE!", - "=WORKDAY(-1,123)": "#NUM!", - "=WORKDAY.INTL()": "WORKDAY.INTL requires at least 2 arguments", - "=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12,\"\")": "WORKDAY.INTL requires at most 4 arguments", - "=WORKDAY.INTL(\"01/01/2020\",\"\",4,B1:B12)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=WORKDAY.INTL(\"\",123,4,B1:B12)": "#VALUE!", - "=WORKDAY.INTL(\"01/01/2020\",123,\"\",B1:B12)": "#VALUE!", - "=WORKDAY.INTL(\"01/01/2020\",123,\"000000x\")": "#VALUE!", - "=WORKDAY.INTL(\"01/01/2020\",123,\"0000002\")": "#VALUE!", - "=WORKDAY.INTL(\"January 25, 100\",123)": "#VALUE!", - "=WORKDAY.INTL(-1,123)": "#NUM!", + calcError := map[string][]string{ + "=NETWORKDAYS()": {"#VALUE!", "NETWORKDAYS requires at least 2 arguments"}, + "=NETWORKDAYS(\"01/01/2020\",\"09/12/2020\",2,\"\")": {"#VALUE!", "NETWORKDAYS requires at most 3 arguments"}, + "=NETWORKDAYS(\"\",\"09/12/2020\",2)": {"#VALUE!", "#VALUE!"}, + "=NETWORKDAYS(\"01/01/2020\",\"\",2)": {"#VALUE!", "#VALUE!"}, + "=NETWORKDAYS.INTL()": {"#VALUE!", "NETWORKDAYS.INTL requires at least 2 arguments"}, + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",4,A1:A12,\"\")": {"#VALUE!", "NETWORKDAYS.INTL requires at most 4 arguments"}, + "=NETWORKDAYS.INTL(\"01/01/2020\",\"January 25, 100\",4)": {"#VALUE!", "#VALUE!"}, + "=NETWORKDAYS.INTL(\"\",123,4,B1:B12)": {"#VALUE!", "#VALUE!"}, + "=NETWORKDAYS.INTL(\"01/01/2020\",123,\"000000x\")": {"#VALUE!", "#VALUE!"}, + "=NETWORKDAYS.INTL(\"01/01/2020\",123,\"0000002\")": {"#VALUE!", "#VALUE!"}, + "=NETWORKDAYS.INTL(\"January 25, 100\",123)": {"#VALUE!", "#VALUE!"}, + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",8)": {"#VALUE!", "#VALUE!"}, + "=NETWORKDAYS.INTL(-1,123)": {"#NUM!", "#NUM!"}, + "=WORKDAY()": {"#VALUE!", "WORKDAY requires at least 2 arguments"}, + "=WORKDAY(\"01/01/2020\",123,A1:A12,\"\")": {"#VALUE!", "WORKDAY requires at most 3 arguments"}, + "=WORKDAY(\"01/01/2020\",\"\",B1:B12)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=WORKDAY(\"\",123,B1:B12)": {"#VALUE!", "#VALUE!"}, + "=WORKDAY(\"January 25, 100\",123)": {"#VALUE!", "#VALUE!"}, + "=WORKDAY(-1,123)": {"#NUM!", "#NUM!"}, + "=WORKDAY.INTL()": {"#VALUE!", "WORKDAY.INTL requires at least 2 arguments"}, + "=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12,\"\")": {"#VALUE!", "WORKDAY.INTL requires at most 4 arguments"}, + "=WORKDAY.INTL(\"01/01/2020\",\"\",4,B1:B12)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=WORKDAY.INTL(\"\",123,4,B1:B12)": {"#VALUE!", "#VALUE!"}, + "=WORKDAY.INTL(\"01/01/2020\",123,\"\",B1:B12)": {"#VALUE!", "#VALUE!"}, + "=WORKDAY.INTL(\"01/01/2020\",123,\"000000x\")": {"#VALUE!", "#VALUE!"}, + "=WORKDAY.INTL(\"01/01/2020\",123,\"0000002\")": {"#VALUE!", "#VALUE!"}, + "=WORKDAY.INTL(\"January 25, 100\",123)": {"#VALUE!", "#VALUE!"}, + "=WORKDAY.INTL(-1,123)": {"#NUM!", "#NUM!"}, } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected, formula) - assert.Equal(t, "", result, formula) + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) } } From 7631fd08e173e6c65267fef06e1f9a258ddafe55 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 18 Mar 2023 15:59:39 +0800 Subject: [PATCH 171/213] This closes #1499, support to set number format for chart data labels and axis --- chart_test.go | 4 ++-- drawing.go | 32 ++++++++++++++++++++++++-------- xmlChart.go | 9 +++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/chart_test.go b/chart_test.go index 1f265a3f94..accfc59052 100644 --- a/chart_test.go +++ b/chart_test.go @@ -220,12 +220,12 @@ func TestAddChart(t *testing.T) { {sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: "col3DCylinderPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AV45", opts: &Chart{Type: "col3DCylinder", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: "col3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: "line3D", Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}}, + {sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: "line3D", Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}}}, {sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: "scatter", Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Scatter Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: "doughnut", Series: series3, Format: format, Legend: ChartLegend{Position: "right", ShowLegendKey: false}, Title: ChartTitle{Name: "Doughnut Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}}, {sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: "line", Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}}, {sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: "pie3D", Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: "pie", Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Pie Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "gap"}}, + {sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: "pie", Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Pie Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false, NumFmt: ChartNumFmt{CustomNumFmt: "0.00%;0;;"}}, ShowBlanksAs: "gap"}}, // bar series chart {sheetName: "Sheet2", cell: "P48", opts: &Chart{Type: "bar", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "X48", opts: &Chart{Type: "barStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, diff --git a/drawing.go b/drawing.go index 93684a3ff2..f035d50a5f 100644 --- a/drawing.go +++ b/drawing.go @@ -995,10 +995,24 @@ func (f *File) drawCharSeriesBubble3D(opts *Chart) *attrValBool { return &attrValBool{Val: boolPtr(true)} } +// drawChartNumFmt provides a function to draw the c:numFmt element by given +// data labels format sets. +func (f *File) drawChartNumFmt(labels ChartNumFmt) *cNumFmt { + var numFmt *cNumFmt + if labels.CustomNumFmt != "" || labels.SourceLinked { + numFmt = &cNumFmt{ + FormatCode: labels.CustomNumFmt, + SourceLinked: labels.SourceLinked, + } + } + return numFmt +} + // drawChartDLbls provides a function to draw the c:dLbls element by given // format sets. func (f *File) drawChartDLbls(opts *Chart) *cDLbls { return &cDLbls{ + NumFmt: f.drawChartNumFmt(opts.PlotArea.NumFmt), ShowLegendKey: &attrValBool{Val: boolPtr(opts.Legend.ShowLegendKey)}, ShowVal: &attrValBool{Val: boolPtr(opts.PlotArea.ShowVal)}, ShowCatName: &attrValBool{Val: boolPtr(opts.PlotArea.ShowCatName)}, @@ -1040,12 +1054,9 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { Max: max, Min: min, }, - Delete: &attrValBool{Val: boolPtr(opts.XAxis.None)}, - AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])}, - NumFmt: &cNumFmt{ - FormatCode: "General", - SourceLinked: true, - }, + Delete: &attrValBool{Val: boolPtr(opts.XAxis.None)}, + AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])}, + NumFmt: &cNumFmt{FormatCode: "General"}, MajorTickMark: &attrValString{Val: stringPtr("none")}, MinorTickMark: &attrValString{Val: stringPtr("none")}, TickLblPos: &attrValString{Val: stringPtr("nextTo")}, @@ -1059,6 +1070,9 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, }, } + if numFmt := f.drawChartNumFmt(opts.XAxis.NumFmt); numFmt != nil { + axs[0].NumFmt = numFmt + } if opts.XAxis.MajorGridLines { axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } @@ -1097,8 +1111,7 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)}, AxPos: &attrValString{Val: stringPtr(valAxPos[opts.YAxis.ReverseOrder])}, NumFmt: &cNumFmt{ - FormatCode: chartValAxNumFmtFormatCode[opts.Type], - SourceLinked: true, + FormatCode: chartValAxNumFmtFormatCode[opts.Type], }, MajorTickMark: &attrValString{Val: stringPtr("none")}, MinorTickMark: &attrValString{Val: stringPtr("none")}, @@ -1110,6 +1123,9 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, }, } + if numFmt := f.drawChartNumFmt(opts.YAxis.NumFmt); numFmt != nil { + axs[0].NumFmt = numFmt + } if opts.YAxis.MajorGridLines { axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } diff --git a/xmlChart.go b/xmlChart.go index 6c17ab8760..56049af6f4 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -481,6 +481,7 @@ type cNumCache struct { // entire series or the entire chart. It contains child elements that specify // the specific formatting and positioning settings. type cDLbls struct { + NumFmt *cNumFmt `xml:"numFmt"` ShowLegendKey *attrValBool `xml:"showLegendKey"` ShowVal *attrValBool `xml:"showVal"` ShowCatName *attrValBool `xml:"showCatName"` @@ -519,6 +520,12 @@ type cPageMargins struct { T float64 `xml:"t,attr"` } +// ChartNumFmt directly maps the number format settings of the chart. +type ChartNumFmt struct { + CustomNumFmt string + SourceLinked bool +} + // ChartAxis directly maps the format settings of the chart axis. type ChartAxis struct { None bool @@ -531,6 +538,7 @@ type ChartAxis struct { Minimum *float64 Font Font LogBase float64 + NumFmt ChartNumFmt } // ChartDimension directly maps the dimension of the chart. @@ -548,6 +556,7 @@ type ChartPlotArea struct { ShowPercent bool ShowSerName bool ShowVal bool + NumFmt ChartNumFmt } // Chart directly maps the format settings of the chart. From 478b528af14840a3abf464a5128471d570f3eaaf Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 19 Mar 2023 20:23:33 +0800 Subject: [PATCH 172/213] Breaking changes: changed the function signature for 2 exported functions - Change `func (f *File) AddPictureFromBytes(sheet, cell, name, extension string, file []byte, opts *GraphicOptions) error` to `func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error` - Change `func (f *File) GetPicture(sheet, cell string) (string, []byte, error)` to `func (f *File) GetPictures(sheet, cell string) ([]Picture, error)` Co-authored-by: huangsk <645636204@qq.com> --- cell_test.go | 5 ++- excelize_test.go | 2 +- picture.go | 82 ++++++++++++++++++++++++++---------------------- picture_test.go | 72 +++++++++++++++++++++--------------------- xmlDrawing.go | 8 +++++ 5 files changed, 91 insertions(+), 78 deletions(-) diff --git a/cell_test.go b/cell_test.go index 210918cfff..17ca800ccf 100644 --- a/cell_test.go +++ b/cell_test.go @@ -52,9 +52,8 @@ func TestConcurrency(t *testing.T) { }, )) // Concurrency get cell picture - name, raw, err := f.GetPicture("Sheet1", "A1") - assert.Equal(t, "", name) - assert.Nil(t, raw) + pics, err := f.GetPictures("Sheet1", "A1") + assert.Len(t, pics, 0) assert.NoError(t, err) // Concurrency iterate rows rows, err := f.Rows("Sheet1") diff --git a/excelize_test.go b/excelize_test.go index 8d9bbc8a5f..e09c4b81ea 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1558,7 +1558,7 @@ func prepareTestBook1() (*File, error) { return nil, err } - err = f.AddPictureFromBytes("Sheet1", "Q1", "Excel Logo", ".jpg", file, nil) + err = f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".jpg", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}) if err != nil { return nil, err } diff --git a/picture.go b/picture.go index 54b03f2a04..fcc8d5fc7f 100644 --- a/picture.go +++ b/picture.go @@ -145,19 +145,18 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { // // The optional parameter "ScaleY" specifies the vertical scale of images, // the default value of that is 1.0 which presents 100%. -func (f *File) AddPicture(sheet, cell, picture string, opts *GraphicOptions) error { +func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error { var err error // Check picture exists first. - if _, err = os.Stat(picture); os.IsNotExist(err) { + if _, err = os.Stat(name); os.IsNotExist(err) { return err } - ext, ok := supportedImageTypes[path.Ext(picture)] + ext, ok := supportedImageTypes[path.Ext(name)] if !ok { return ErrImgExt } - file, _ := os.ReadFile(filepath.Clean(picture)) - _, name := filepath.Split(picture) - return f.AddPictureFromBytes(sheet, cell, name, ext, file, opts) + file, _ := os.ReadFile(filepath.Clean(name)) + return f.AddPictureFromBytes(sheet, cell, &Picture{Extension: ext, File: file, Format: opts}) } // AddPictureFromBytes provides the method to add picture in a sheet by given @@ -188,7 +187,11 @@ func (f *File) AddPicture(sheet, cell, picture string, opts *GraphicOptions) err // fmt.Println(err) // return // } -// if err := f.AddPictureFromBytes("Sheet1", "A2", "Excel Logo", ".jpg", file, nil); err != nil { +// if err := f.AddPictureFromBytes("Sheet1", "A2", &excelize.Picture{ +// Extension: ".jpg", +// File: file, +// Format: &excelize.GraphicOptions{AltText: "Excel Logo"}, +// }); err != nil { // fmt.Println(err) // return // } @@ -196,15 +199,15 @@ func (f *File) AddPicture(sheet, cell, picture string, opts *GraphicOptions) err // fmt.Println(err) // } // } -func (f *File) AddPictureFromBytes(sheet, cell, name, extension string, file []byte, opts *GraphicOptions) error { +func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error { var drawingHyperlinkRID int var hyperlinkType string - ext, ok := supportedImageTypes[extension] + ext, ok := supportedImageTypes[pic.Extension] if !ok { return ErrImgExt } - options := parseGraphicOptions(opts) - img, _, err := image.DecodeConfig(bytes.NewReader(file)) + options := parseGraphicOptions(pic.Format) + img, _, err := image.DecodeConfig(bytes.NewReader(pic.File)) if err != nil { return err } @@ -219,7 +222,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, name, extension string, file []b drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML) drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" - mediaStr := ".." + strings.TrimPrefix(f.addMedia(file, ext), "xl") + mediaStr := ".." + strings.TrimPrefix(f.addMedia(pic.File, ext), "xl") drawingRID := f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType) // Add picture with hyperlink. if options.Hyperlink != "" && options.HyperlinkType != "" { @@ -229,7 +232,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, name, extension string, file []b drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, options.Hyperlink, hyperlinkType) } ws.Unlock() - err = f.addDrawingPicture(sheet, drawingXML, cell, name, ext, drawingRID, drawingHyperlinkRID, img, options) + err = f.addDrawingPicture(sheet, drawingXML, cell, ext, drawingRID, drawingHyperlinkRID, img, options) if err != nil { return err } @@ -319,7 +322,7 @@ func (f *File) countDrawings() int { // addDrawingPicture provides a function to add picture by given sheet, // drawingXML, cell, file name, width, height relationship index and format // sets. -func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, hyperlinkRID int, img image.Config, opts *GraphicOptions) error { +func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyperlinkRID int, img image.Config, opts *GraphicOptions) error { col, row, err := CellNameToCoordinates(cell) if err != nil { return err @@ -358,7 +361,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, pic := xlsxPic{} pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = opts.LockAspectRatio pic.NvPicPr.CNvPr.ID = cNvPrID - pic.NvPicPr.CNvPr.Descr = file + pic.NvPicPr.CNvPr.Descr = opts.AltText pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID) if hyperlinkRID != 0 { pic.NvPicPr.CNvPr.HlinkClick = &xlsxHlinkClick{ @@ -556,10 +559,10 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { return "" } -// GetPicture provides a function to get picture base name and raw content +// GetPictures provides a function to get picture meta info and raw content // embed in spreadsheet by given worksheet and cell name. This function -// returns the file name in spreadsheet and file contents as []byte data -// types. This function is concurrency safe. For example: +// returns the image contents as []byte data types. This function is +// concurrency safe. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { @@ -571,27 +574,29 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // fmt.Println(err) // } // }() -// file, raw, err := f.GetPicture("Sheet1", "A2") +// pics, err := f.GetPictures("Sheet1", "A2") // if err != nil { -// fmt.Println(err) -// return +// fmt.Println(err) // } -// if err := os.WriteFile(file, raw, 0644); err != nil { -// fmt.Println(err) +// for idx, pic := range pics { +// name := fmt.Sprintf("image%d%s", idx+1, pic.Extension) +// if err := os.WriteFile(name, pic.File, 0644); err != nil { +// fmt.Println(err) +// } // } -func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { +func (f *File) GetPictures(sheet, cell string) ([]Picture, error) { col, row, err := CellNameToCoordinates(cell) if err != nil { - return "", nil, err + return nil, err } col-- row-- ws, err := f.workSheetReader(sheet) if err != nil { - return "", nil, err + return nil, err } if ws.Drawing == nil { - return "", nil, err + return nil, err } target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingXML := strings.ReplaceAll(target, "..", "xl") @@ -601,7 +606,7 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { return f.getPicture(row, col, drawingXML, drawingRelationships) } -// DeletePicture provides a function to delete charts in spreadsheet by given +// DeletePicture provides a function to delete all pictures in a cell by given // worksheet name and cell reference. Note that the image file won't be deleted // from the document currently. func (f *File) DeletePicture(sheet, cell string) error { @@ -624,7 +629,7 @@ func (f *File) DeletePicture(sheet, cell string) error { // getPicture provides a function to get picture base name and raw content // embed in spreadsheet by given coordinates and drawing relationships. -func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (ret string, buf []byte, err error) { +func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (pics []Picture, err error) { var ( wsDr *xlsxWsDr ok bool @@ -636,7 +641,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) if wsDr, _, err = f.drawingParser(drawingXML); err != nil { return } - if ret, buf = f.getPictureFromWsDr(row, col, drawingRelationships, wsDr); len(buf) > 0 { + if pics = f.getPicturesFromWsDr(row, col, drawingRelationships, wsDr); len(pics) > 0 { return } deWsDr = new(decodeWsDr) @@ -655,9 +660,11 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed) if _, ok = supportedImageTypes[filepath.Ext(drawRel.Target)]; ok { - ret = filepath.Base(drawRel.Target) + pic := Picture{Extension: filepath.Ext(drawRel.Target), Format: &GraphicOptions{}} if buffer, _ := f.Pkg.Load(strings.ReplaceAll(drawRel.Target, "..", "xl")); buffer != nil { - buf = buffer.([]byte) + pic.File = buffer.([]byte) + pic.Format.AltText = deTwoCellAnchor.Pic.NvPicPr.CNvPr.Descr + pics = append(pics, pic) } return } @@ -667,10 +674,10 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) return } -// getPictureFromWsDr provides a function to get picture base name and raw +// getPicturesFromWsDr provides a function to get picture base name and raw // content in worksheet drawing by given coordinates and drawing // relationships. -func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsDr *xlsxWsDr) (ret string, buf []byte) { +func (f *File) getPicturesFromWsDr(row, col int, drawingRelationships string, wsDr *xlsxWsDr) (pics []Picture) { var ( ok bool anchor *xdrCellAnchor @@ -684,11 +691,12 @@ func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsD if drawRel = f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed); drawRel != nil { if _, ok = supportedImageTypes[filepath.Ext(drawRel.Target)]; ok { - ret = filepath.Base(drawRel.Target) + pic := Picture{Extension: filepath.Ext(drawRel.Target), Format: &GraphicOptions{}} if buffer, _ := f.Pkg.Load(strings.ReplaceAll(drawRel.Target, "..", "xl")); buffer != nil { - buf = buffer.([]byte) + pic.File = buffer.([]byte) + pic.Format.AltText = anchor.Pic.NvPicPr.CNvPr.Descr + pics = append(pics, pic) } - return } } } diff --git a/picture_test.go b/picture_test.go index d6b91c7f7f..95bb39e9fa 100644 --- a/picture_test.go +++ b/picture_test.go @@ -26,7 +26,7 @@ func BenchmarkAddPictureFromBytes(b *testing.B) { } b.ResetTimer() for i := 1; i <= b.N; i++ { - if err := f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "excel", ".png", imgFile, nil); err != nil { + if err := f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), &Picture{Extension: ".png", File: imgFile, Format: &GraphicOptions{AltText: "Excel"}}); err != nil { b.Error(err) } } @@ -58,9 +58,9 @@ func TestAddPicture(t *testing.T) { assert.NoError(t, f.AddPicture("AddPicture", "A1", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true})) // Test add picture to worksheet from bytes - assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "Excel Logo", ".png", file, nil)) + assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}})) // Test add picture to worksheet from bytes with illegal cell reference - assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", "Excel Logo", ".png", file, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.AddPicture("Sheet1", "Q8", filepath.Join("test", "images", "excel.gif"), nil)) assert.NoError(t, f.AddPicture("Sheet1", "Q15", filepath.Join("test", "images", "excel.jpg"), nil)) @@ -75,7 +75,7 @@ func TestAddPicture(t *testing.T) { f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "Q1", "Excel Logo", ".png", file, nil), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}), "XML syntax error on line 1: invalid UTF-8") // Test add picture with invalid sheet name assert.EqualError(t, f.AddPicture("Sheet:1", "A1", filepath.Join("test", "images", "excel.jpg"), nil), ErrSheetNameInvalid.Error()) @@ -90,10 +90,11 @@ func TestAddPictureErrors(t *testing.T) { // Test add picture to worksheet with unsupported file type assert.EqualError(t, f.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), nil), ErrImgExt.Error()) - assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", "Excel Logo", "jpg", make([]byte, 1), nil), ErrImgExt.Error()) + + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", &Picture{Extension: "jpg", File: make([]byte, 1), Format: &GraphicOptions{AltText: "Excel Logo"}}), ErrImgExt.Error()) // Test add picture to worksheet with invalid file data - assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", "Excel Logo", ".jpg", make([]byte, 1), nil), image.ErrFormat.Error()) + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", &Picture{Extension: ".jpg", File: make([]byte, 1), Format: &GraphicOptions{AltText: "Excel Logo"}}), image.ErrFormat.Error()) // Test add picture with custom image decoder and encoder decode := func(r io.Reader) (image.Image, error) { return nil, nil } @@ -115,41 +116,39 @@ func TestAddPictureErrors(t *testing.T) { func TestGetPicture(t *testing.T) { f := NewFile() assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil)) - name, content, err := f.GetPicture("Sheet1", "A1") + pics, err := f.GetPictures("Sheet1", "A1") assert.NoError(t, err) - assert.Equal(t, 13233, len(content)) - assert.Equal(t, "image1.png", name) + assert.Len(t, pics[0].File, 13233) + assert.Empty(t, pics[0].Format.AltText) f, err = prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } - file, raw, err := f.GetPicture("Sheet1", "F21") + pics, err = f.GetPictures("Sheet1", "F21") assert.NoError(t, err) - if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) || - !assert.NoError(t, os.WriteFile(filepath.Join("test", file), raw, 0o644)) { + if !assert.NotEmpty(t, filepath.Join("test", fmt.Sprintf("image1%s", pics[0].Extension))) || !assert.NotEmpty(t, pics[0].File) || + !assert.NoError(t, os.WriteFile(filepath.Join("test", fmt.Sprintf("image1%s", pics[0].Extension)), pics[0].File, 0o644)) { t.FailNow() } // Try to get picture from a worksheet with illegal cell reference - _, _, err = f.GetPicture("Sheet1", "A") + _, err = f.GetPictures("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // Try to get picture from a worksheet that doesn't contain any images - file, raw, err = f.GetPicture("Sheet3", "I9") + pics, err = f.GetPictures("Sheet3", "I9") assert.EqualError(t, err, "sheet Sheet3 does not exist") - assert.Empty(t, file) - assert.Empty(t, raw) + assert.Len(t, pics, 0) // Try to get picture from a cell that doesn't contain an image - file, raw, err = f.GetPicture("Sheet2", "A2") + pics, err = f.GetPictures("Sheet2", "A2") assert.NoError(t, err) - assert.Empty(t, file) - assert.Empty(t, raw) + assert.Len(t, pics, 0) // Test get picture with invalid sheet name - _, _, err = f.GetPicture("Sheet:1", "A2") + _, err = f.GetPictures("Sheet:1", "A2") assert.EqualError(t, err, ErrSheetNameInvalid.Error()) f.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8") @@ -163,36 +162,34 @@ func TestGetPicture(t *testing.T) { f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) assert.NoError(t, err) - file, raw, err = f.GetPicture("Sheet1", "F21") + pics, err = f.GetPictures("Sheet1", "F21") assert.NoError(t, err) - if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) || - !assert.NoError(t, os.WriteFile(filepath.Join("test", file), raw, 0o644)) { + if !assert.NotEmpty(t, filepath.Join("test", fmt.Sprintf("image1%s", pics[0].Extension))) || !assert.NotEmpty(t, pics[0].File) || + !assert.NoError(t, os.WriteFile(filepath.Join("test", fmt.Sprintf("image1%s", pics[0].Extension)), pics[0].File, 0o644)) { t.FailNow() } // Try to get picture from a local storage file that doesn't contain an image - file, raw, err = f.GetPicture("Sheet1", "F22") + pics, err = f.GetPictures("Sheet1", "F22") assert.NoError(t, err) - assert.Empty(t, file) - assert.Empty(t, raw) + assert.Len(t, pics, 0) assert.NoError(t, f.Close()) // Test get picture from none drawing worksheet f = NewFile() - file, raw, err = f.GetPicture("Sheet1", "F22") + pics, err = f.GetPictures("Sheet1", "F22") assert.NoError(t, err) - assert.Empty(t, file) - assert.Empty(t, raw) + assert.Len(t, pics, 0) f, err = prepareTestBook1() assert.NoError(t, err) // Test get pictures with unsupported charset path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) - _, _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels") + _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") f.Drawings.Delete(path) - _, _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels") + _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } @@ -200,19 +197,20 @@ func TestAddDrawingPicture(t *testing.T) { // Test addDrawingPicture with illegal cell reference f := NewFile() opts := &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)} - assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", "", 0, 0, image.Config{}, opts), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, image.Config{}, opts), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) - assert.EqualError(t, f.addDrawingPicture("sheet1", path, "A1", "", "", 0, 0, image.Config{}, opts), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addDrawingPicture("sheet1", path, "A1", "", 0, 0, image.Config{}, opts), "XML syntax error on line 1: invalid UTF-8") } func TestAddPictureFromBytes(t *testing.T) { f := NewFile() imgFile, err := os.ReadFile("logo.png") assert.NoError(t, err, "Unable to load logo for test") - assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "logo", ".png", imgFile, nil)) - assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "logo", ".png", imgFile, nil)) + + assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), &Picture{Extension: ".png", File: imgFile, Format: &GraphicOptions{AltText: "logo"}})) + assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), &Picture{Extension: ".png", File: imgFile, Format: &GraphicOptions{AltText: "logo"}})) imageCount := 0 f.Pkg.Range(func(fileName, v interface{}) bool { if strings.Contains(fileName.(string), "media/image") { @@ -221,9 +219,9 @@ func TestAddPictureFromBytes(t *testing.T) { return true }) assert.Equal(t, 1, imageCount, "Duplicate image should only be stored once.") - assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), "logo", ".png", imgFile, nil), "sheet SheetN does not exist") + assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), &Picture{Extension: ".png", File: imgFile, Format: &GraphicOptions{AltText: "logo"}}), "sheet SheetN does not exist") // Test add picture from bytes with invalid sheet name - assert.EqualError(t, f.AddPictureFromBytes("Sheet:1", fmt.Sprint("A", 1), "logo", ".png", imgFile, nil), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddPictureFromBytes("Sheet:1", fmt.Sprint("A", 1), &Picture{Extension: ".png", File: imgFile, Format: &GraphicOptions{AltText: "logo"}}), ErrSheetNameInvalid.Error()) } func TestDeletePicture(t *testing.T) { diff --git a/xmlDrawing.go b/xmlDrawing.go index 3130833f10..caf9897ae3 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -581,8 +581,16 @@ type xdrTxBody struct { P []*aP `xml:"a:p"` } +// Picture maps the format settings of the picture. +type Picture struct { + Extension string + File []byte + Format *GraphicOptions +} + // GraphicOptions directly maps the format settings of the picture. type GraphicOptions struct { + AltText string PrintObject *bool Locked *bool LockAspectRatio bool From a34c81e1ccbfdfa082fe63d6fa0f68510a4ba725 Mon Sep 17 00:00:00 2001 From: ChantXu64 Date: Mon, 20 Mar 2023 09:17:28 +0800 Subject: [PATCH 173/213] This closes #1448, speed up for checking merged cells (#1500) --- cell.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/cell.go b/cell.go index a23296b65e..3fdbea96e0 100644 --- a/cell.go +++ b/cell.go @@ -1388,6 +1388,10 @@ func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int { // given cell reference. func (f *File) mergeCellsParser(ws *xlsxWorksheet, cell string) (string, error) { cell = strings.ToUpper(cell) + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return cell, err + } if ws.MergeCells != nil { for i := 0; i < len(ws.MergeCells.Cells); i++ { if ws.MergeCells.Cells[i] == nil { @@ -1395,12 +1399,20 @@ func (f *File) mergeCellsParser(ws *xlsxWorksheet, cell string) (string, error) i-- continue } - ok, err := f.checkCellInRangeRef(cell, ws.MergeCells.Cells[i].Ref) - if err != nil { - return cell, err + if ref := ws.MergeCells.Cells[i].Ref; len(ws.MergeCells.Cells[i].rect) == 0 && ref != "" { + if strings.Count(ref, ":") != 1 { + ref += ":" + ref + } + rect, err := rangeRefToCoordinates(ref) + if err != nil { + return cell, err + } + _ = sortCoordinates(rect) + ws.MergeCells.Cells[i].rect = rect } - if ok { + if cellInRange([]int{col, row}, ws.MergeCells.Cells[i].rect) { cell = strings.Split(ws.MergeCells.Cells[i].Ref, ":")[0] + break } } } From 5878fbd282c13b3ab74cccdba0927a0fac1f1792 Mon Sep 17 00:00:00 2001 From: playGitboy <72111157+playGitboy@users.noreply.github.com> Date: Thu, 23 Mar 2023 00:06:10 +0800 Subject: [PATCH 174/213] This closes #1503, case-insensitive for the file extension name --- file.go | 5 +++-- picture.go | 8 ++++---- sheet.go | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/file.go b/file.go index 51cd320d33..2896ee44fc 100644 --- a/file.go +++ b/file.go @@ -18,6 +18,7 @@ import ( "io" "os" "path/filepath" + "strings" "sync" ) @@ -73,7 +74,7 @@ func (f *File) SaveAs(name string, opts ...Options) error { return ErrMaxFilePathLength } f.Path = name - if _, ok := supportedContentTypes[filepath.Ext(f.Path)]; !ok { + if _, ok := supportedContentTypes[strings.ToLower(filepath.Ext(f.Path))]; !ok { return ErrWorkbookFileFormat } file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) @@ -116,7 +117,7 @@ func (f *File) WriteTo(w io.Writer, opts ...Options) (int64, error) { f.options = &opts[i] } if len(f.Path) != 0 { - contentType, ok := supportedContentTypes[filepath.Ext(f.Path)] + contentType, ok := supportedContentTypes[strings.ToLower(filepath.Ext(f.Path))] if !ok { return 0, ErrWorkbookFileFormat } diff --git a/picture.go b/picture.go index fcc8d5fc7f..edf53732c1 100644 --- a/picture.go +++ b/picture.go @@ -151,7 +151,7 @@ func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error if _, err = os.Stat(name); os.IsNotExist(err) { return err } - ext, ok := supportedImageTypes[path.Ext(name)] + ext, ok := supportedImageTypes[strings.ToLower(path.Ext(name))] if !ok { return ErrImgExt } @@ -202,7 +202,7 @@ func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error { var drawingHyperlinkRID int var hyperlinkType string - ext, ok := supportedImageTypes[pic.Extension] + ext, ok := supportedImageTypes[strings.ToLower(pic.Extension)] if !ok { return ErrImgExt } @@ -659,7 +659,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic != nil { if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed) - if _, ok = supportedImageTypes[filepath.Ext(drawRel.Target)]; ok { + if _, ok = supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok { pic := Picture{Extension: filepath.Ext(drawRel.Target), Format: &GraphicOptions{}} if buffer, _ := f.Pkg.Load(strings.ReplaceAll(drawRel.Target, "..", "xl")); buffer != nil { pic.File = buffer.([]byte) @@ -690,7 +690,7 @@ func (f *File) getPicturesFromWsDr(row, col int, drawingRelationships string, ws if anchor.From.Col == col && anchor.From.Row == row { if drawRel = f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed); drawRel != nil { - if _, ok = supportedImageTypes[filepath.Ext(drawRel.Target)]; ok { + if _, ok = supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok { pic := Picture{Extension: filepath.Ext(drawRel.Target), Format: &GraphicOptions{}} if buffer, _ := f.Pkg.Load(strings.ReplaceAll(drawRel.Target, "..", "xl")); buffer != nil { pic.File = buffer.([]byte) diff --git a/sheet.go b/sheet.go index 0bc6815452..a6e3d032b8 100644 --- a/sheet.go +++ b/sheet.go @@ -525,7 +525,7 @@ func (f *File) SetSheetBackgroundFromBytes(sheet, extension string, picture []by // setSheetBackground provides a function to set background picture by given // worksheet name, file name extension and image data. func (f *File) setSheetBackground(sheet, extension string, file []byte) error { - imageType, ok := supportedImageTypes[extension] + imageType, ok := supportedImageTypes[strings.ToLower(extension)] if !ok { return ErrImgExt } From 60b9d029a672634299fdab880423ba48a165507b Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 25 Mar 2023 13:30:13 +0800 Subject: [PATCH 175/213] Breaking changes: changed the function signature for 4 exported functions - Change `func (f *File) AddVBAProject(bin string) error` to `func (f *File) AddVBAProject(file []byte) error` - Change `func (f *File) GetComments() (map[string][]Comment, error)` to `func (f *File) GetComments(sheet string) ([]Comment, error)` - Change `func (f *File) AddTable(sheet, rangeRef string, opts *TableOptions) error` to `func (f *File) AddTable(sheet string, table *Table) error` - Change `func (sw *StreamWriter) AddTable(rangeRef string, opts *TableOptions) error` to `func (sw *StreamWriter) AddTable(table *Table) error` - Rename exported data type `TableOptions` to `Table` - Simplify the assert statements in the unit tests - Update documents for the functions - Update unit tests --- adjust_test.go | 29 +++++++++-------- cell.go | 4 +-- cell_test.go | 2 +- comment.go | 71 ++++++++++++++++++++--------------------- comment_test.go | 41 ++++++++++++++---------- datavalidation_test.go | 10 +++--- errors.go | 2 +- excelize.go | 23 +++++++------ excelize_test.go | 35 ++++++++++---------- file.go | 4 --- lib_test.go | 2 +- merge_test.go | 2 +- shape.go | 2 +- sheet_test.go | 4 +-- stream.go | 15 +++++---- stream_test.go | 14 ++++---- table.go | 17 +++++----- table_test.go | 25 +++++++++------ test/vbaProject.bin | Bin 16896 -> 15360 bytes xmlTable.go | 5 +-- 20 files changed, 162 insertions(+), 145 deletions(-) mode change 100755 => 100644 test/vbaProject.bin diff --git a/adjust_test.go b/adjust_test.go index 7b992419a9..f55ef4b7eb 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -11,7 +11,7 @@ import ( func TestAdjustMergeCells(t *testing.T) { f := NewFile() // Test adjustAutoFilter with illegal cell reference - assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{ + assert.Equal(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ { @@ -19,8 +19,8 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, - }, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{ + }, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) + assert.Equal(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ { @@ -28,7 +28,7 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, - }, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + }, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B"))) assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -272,7 +272,7 @@ func TestAdjustMergeCells(t *testing.T) { } for _, c := range cases { assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, -1)) - assert.Equal(t, 0, len(c.ws.MergeCells.Cells), c.label) + assert.Len(t, c.ws.MergeCells.Cells, 0, c.label) } f = NewFile() @@ -293,22 +293,23 @@ func TestAdjustAutoFilter(t *testing.T) { }, }, rows, 1, -1)) // Test adjustAutoFilter with illegal cell reference - assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{ + assert.Equal(t, f.adjustAutoFilter(&xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{ Ref: "A:B1", }, - }, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{ + }, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) + assert.Equal(t, f.adjustAutoFilter(&xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{ Ref: "A1:B", }, - }, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + }, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B"))) } func TestAdjustTable(t *testing.T) { f, sheetName := NewFile(), "Sheet1" for idx, reference := range []string{"B2:C3", "E3:F5", "H5:H8", "J5:K9"} { - assert.NoError(t, f.AddTable(sheetName, reference, &TableOptions{ + assert.NoError(t, f.AddTable(sheetName, &Table{ + Range: reference, Name: fmt.Sprintf("table%d", idx), StyleName: "TableStyleMedium2", ShowFirstColumn: true, @@ -323,7 +324,7 @@ func TestAdjustTable(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustTable.xlsx"))) f = NewFile() - assert.NoError(t, f.AddTable(sheetName, "A1:D5", nil)) + assert.NoError(t, f.AddTable(sheetName, &Table{Range: "A1:D5"})) // Test adjust table with non-table part f.Pkg.Delete("xl/tables/table1.xml") assert.NoError(t, f.RemoveRow(sheetName, 1)) @@ -346,8 +347,8 @@ func TestAdjustHelper(t *testing.T) { AutoFilter: &xlsxAutoFilter{Ref: "A1:B"}, }) // Test adjustHelper with illegal cell reference - assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + assert.Equal(t, f.adjustHelper("Sheet1", rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) + assert.Equal(t, f.adjustHelper("Sheet2", rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B"))) // Test adjustHelper on not exists worksheet assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN does not exist") } @@ -363,7 +364,7 @@ func TestAdjustCalcChain(t *testing.T) { assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) f.CalcChain.C[1].R = "invalid coordinates" - assert.EqualError(t, f.InsertCols("Sheet1", "A", 1), newCellNameToCoordinatesError("invalid coordinates", newInvalidCellNameError("invalid coordinates")).Error()) + assert.Equal(t, f.InsertCols("Sheet1", "A", 1), newCellNameToCoordinatesError("invalid coordinates", newInvalidCellNameError("invalid coordinates"))) f.CalcChain = nil assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) } diff --git a/cell.go b/cell.go index 3fdbea96e0..5ebcefee6c 100644 --- a/cell.go +++ b/cell.go @@ -694,8 +694,8 @@ type FormulaOpts struct { // return // } // } -// if err := f.AddTable("Sheet1", "A1:C2", &excelize.TableOptions{ -// Name: "Table1", StyleName: "TableStyleMedium2", +// if err := f.AddTable("Sheet1", &excelize.Table{ +// Range: "A1:C2", Name: "Table1", StyleName: "TableStyleMedium2", // }); err != nil { // fmt.Println(err) // return diff --git a/cell_test.go b/cell_test.go index 17ca800ccf..58a4bee62a 100644 --- a/cell_test.go +++ b/cell_test.go @@ -571,7 +571,7 @@ func TestSetCellFormula(t *testing.T) { for idx, row := range [][]interface{}{{"A", "B", "C"}, {1, 2}} { assert.NoError(t, f.SetSheetRow("Sheet1", fmt.Sprintf("A%d", idx+1), &row)) } - assert.NoError(t, f.AddTable("Sheet1", "A1:C2", &TableOptions{Name: "Table1", StyleName: "TableStyleMedium2"})) + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:C2", Name: "Table1", StyleName: "TableStyleMedium2"})) formulaType = STCellFormulaTypeDataTable assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=SUM(Table1[[A]:[B]])", FormulaOpts{Type: &formulaType})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula6.xlsx"))) diff --git a/comment.go b/comment.go index 40912d07ad..39f11762e8 100644 --- a/comment.go +++ b/comment.go @@ -21,46 +21,43 @@ import ( "strings" ) -// GetComments retrieves all comments and returns a map of worksheet name to -// the worksheet comments. -func (f *File) GetComments() (map[string][]Comment, error) { - comments := map[string][]Comment{} - for n, path := range f.sheetMap { - target := f.getSheetComments(filepath.Base(path)) - if target == "" { - continue - } - if !strings.HasPrefix(target, "/") { - target = "xl" + strings.TrimPrefix(target, "..") - } - cmts, err := f.commentsReader(strings.TrimPrefix(target, "/")) - if err != nil { - return comments, err - } - if cmts != nil { - var sheetComments []Comment - for _, comment := range cmts.CommentList.Comment { - sheetComment := Comment{} - if comment.AuthorID < len(cmts.Authors.Author) { - sheetComment.Author = cmts.Authors.Author[comment.AuthorID] - } - sheetComment.Cell = comment.Ref - sheetComment.AuthorID = comment.AuthorID - if comment.Text.T != nil { - sheetComment.Text += *comment.Text.T - } - for _, text := range comment.Text.R { - if text.T != nil { - run := RichTextRun{Text: text.T.Val} - if text.RPr != nil { - run.Font = newFont(text.RPr) - } - sheetComment.Runs = append(sheetComment.Runs, run) +// GetComments retrieves all comments in a worksheet by given worksheet name. +func (f *File) GetComments(sheet string) ([]Comment, error) { + var comments []Comment + sheetXMLPath, ok := f.getSheetXMLPath(sheet) + if !ok { + return comments, newNoExistSheetError(sheet) + } + commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath)) + if !strings.HasPrefix(commentsXML, "/") { + commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..") + } + commentsXML = strings.TrimPrefix(commentsXML, "/") + cmts, err := f.commentsReader(commentsXML) + if err != nil { + return comments, err + } + if cmts != nil { + for _, cmt := range cmts.CommentList.Comment { + comment := Comment{} + if cmt.AuthorID < len(cmts.Authors.Author) { + comment.Author = cmts.Authors.Author[cmt.AuthorID] + } + comment.Cell = cmt.Ref + comment.AuthorID = cmt.AuthorID + if cmt.Text.T != nil { + comment.Text += *cmt.Text.T + } + for _, text := range cmt.Text.R { + if text.T != nil { + run := RichTextRun{Text: text.T.Val} + if text.RPr != nil { + run.Font = newFont(text.RPr) } + comment.Runs = append(comment.Runs, run) } - sheetComments = append(sheetComments, sheetComment) } - comments[n] = sheetComments + comments = append(comments, comment) } } return comments, nil diff --git a/comment_test.go b/comment_test.go index 7acce4820d..b6ea2aa042 100644 --- a/comment_test.go +++ b/comment_test.go @@ -34,21 +34,25 @@ func TestAddComment(t *testing.T) { assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist") // Test add comment on with illegal cell reference assert.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - comments, err := f.GetComments() + comments, err := f.GetComments("Sheet1") assert.NoError(t, err) - if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { - assert.Len(t, comments, 2) - } + assert.Len(t, comments, 2) + comments, err = f.GetComments("Sheet2") + assert.NoError(t, err) + assert.Len(t, comments, 1) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) f.Comments["xl/comments2.xml"] = nil f.Pkg.Store("xl/comments2.xml", []byte(xml.Header+`Excelize: Excelize: `)) - comments, err = f.GetComments() + comments, err = f.GetComments("Sheet1") assert.NoError(t, err) - assert.EqualValues(t, 2, len(comments["Sheet1"])) - assert.EqualValues(t, 1, len(comments["Sheet2"])) - comments, err = NewFile().GetComments() + assert.Len(t, comments, 2) + comments, err = f.GetComments("Sheet2") assert.NoError(t, err) - assert.EqualValues(t, len(comments), 0) + assert.Len(t, comments, 1) + comments, err = NewFile().GetComments("Sheet1") + assert.NoError(t, err) + assert.Len(t, comments, 0) // Test add comments with invalid sheet name assert.EqualError(t, f.AddComment("Sheet:1", Comment{Cell: "A1", Author: "Excelize", Text: "This is a comment."}), ErrSheetNameInvalid.Error()) @@ -56,7 +60,7 @@ func TestAddComment(t *testing.T) { // Test add comments with unsupported charset f.Comments["xl/comments2.xml"] = nil f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset) - _, err = f.GetComments() + _, err = f.GetComments("Sheet2") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") // Test add comments with unsupported charset @@ -68,6 +72,11 @@ func TestAddComment(t *testing.T) { f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8") + + // Test get comments on not exists worksheet + comments, err = f.GetComments("SheetN") + assert.Len(t, comments, 0) + assert.EqualError(t, err, "sheet SheetN does not exist") } func TestDeleteComment(t *testing.T) { @@ -85,13 +94,13 @@ func TestDeleteComment(t *testing.T) { assert.NoError(t, f.DeleteComment("Sheet2", "A40")) - comments, err := f.GetComments() + comments, err := f.GetComments("Sheet2") assert.NoError(t, err) - assert.EqualValues(t, 5, len(comments["Sheet2"])) + assert.Len(t, comments, 5) - comments, err = NewFile().GetComments() + comments, err = NewFile().GetComments("Sheet1") assert.NoError(t, err) - assert.EqualValues(t, len(comments), 0) + assert.Len(t, comments, 0) // Test delete comment with invalid sheet name assert.EqualError(t, f.DeleteComment("Sheet:1", "A1"), ErrSheetNameInvalid.Error()) @@ -99,9 +108,9 @@ func TestDeleteComment(t *testing.T) { assert.NoError(t, f.DeleteComment("Sheet2", "A41")) assert.NoError(t, f.DeleteComment("Sheet2", "C41")) assert.NoError(t, f.DeleteComment("Sheet2", "C42")) - comments, err = f.GetComments() + comments, err = f.GetComments("Sheet2") assert.NoError(t, err) - assert.EqualValues(t, 0, len(comments["Sheet2"])) + assert.EqualValues(t, 0, len(comments)) // Test delete comment on not exists worksheet assert.EqualError(t, f.DeleteComment("SheetN", "A1"), "sheet SheetN does not exist") // Test delete comment with worksheet part diff --git a/datavalidation_test.go b/datavalidation_test.go index 4987f81864..6b705918fb 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -35,7 +35,7 @@ func TestDataValidation(t *testing.T) { dataValidations, err := f.GetDataValidations("Sheet1") assert.NoError(t, err) - assert.Equal(t, len(dataValidations), 1) + assert.Len(t, dataValidations, 1) assert.NoError(t, f.SaveAs(resultFile)) @@ -47,7 +47,7 @@ func TestDataValidation(t *testing.T) { dataValidations, err = f.GetDataValidations("Sheet1") assert.NoError(t, err) - assert.Equal(t, len(dataValidations), 2) + assert.Len(t, dataValidations, 2) assert.NoError(t, f.SaveAs(resultFile)) @@ -62,10 +62,10 @@ func TestDataValidation(t *testing.T) { assert.NoError(t, f.AddDataValidation("Sheet2", dv)) dataValidations, err = f.GetDataValidations("Sheet1") assert.NoError(t, err) - assert.Equal(t, len(dataValidations), 2) + assert.Len(t, dataValidations, 2) dataValidations, err = f.GetDataValidations("Sheet2") assert.NoError(t, err) - assert.Equal(t, len(dataValidations), 1) + assert.Len(t, dataValidations, 1) dv = NewDataValidation(true) dv.Sqref = "A5:B6" @@ -87,7 +87,7 @@ func TestDataValidation(t *testing.T) { dataValidations, err = f.GetDataValidations("Sheet1") assert.NoError(t, err) - assert.Equal(t, len(dataValidations), 3) + assert.Len(t,dataValidations, 3) // Test get data validation on no exists worksheet _, err = f.GetDataValidations("SheetN") diff --git a/errors.go b/errors.go index 1357d0f8e7..2e86470afb 100644 --- a/errors.go +++ b/errors.go @@ -129,7 +129,7 @@ var ( ErrInvalidFormula = errors.New("formula not valid") // ErrAddVBAProject defined the error message on add the VBA project in // the workbook. - ErrAddVBAProject = errors.New("unsupported VBA project extension") + ErrAddVBAProject = errors.New("unsupported VBA project") // ErrMaxRows defined the error message on receive a row number exceeds maximum limit. ErrMaxRows = errors.New("row number exceeds maximum limit") // ErrMaxRowHeight defined the error message on receive an invalid row diff --git a/excelize.go b/excelize.go index a2b61a779f..51ae99b9cd 100644 --- a/excelize.go +++ b/excelize.go @@ -16,10 +16,8 @@ import ( "archive/zip" "bytes" "encoding/xml" - "fmt" "io" "os" - "path" "path/filepath" "strconv" "strings" @@ -460,27 +458,33 @@ func (f *File) UpdateLinkedValue() error { } // AddVBAProject provides the method to add vbaProject.bin file which contains -// functions and/or macros. The file extension should be .xlsm. For example: +// functions and/or macros. The file extension should be XLSM or XLTM. For +// example: // // codeName := "Sheet1" // if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{ // CodeName: &codeName, // }); err != nil { // fmt.Println(err) +// return // } -// if err := f.AddVBAProject("vbaProject.bin"); err != nil { +// file, err := os.ReadFile("vbaProject.bin") +// if err != nil { // fmt.Println(err) +// return +// } +// if err := f.AddVBAProject(file); err != nil { +// fmt.Println(err) +// return // } // if err := f.SaveAs("macros.xlsm"); err != nil { // fmt.Println(err) +// return // } -func (f *File) AddVBAProject(bin string) error { +func (f *File) AddVBAProject(file []byte) error { var err error // Check vbaProject.bin exists first. - if _, err = os.Stat(bin); os.IsNotExist(err) { - return fmt.Errorf("stat %s: no such file or directory", bin) - } - if path.Ext(bin) != ".bin" { + if !bytes.Contains(file, oleIdentifier) { return ErrAddVBAProject } rels, err := f.relsReader(f.getWorkbookRelsPath()) @@ -509,7 +513,6 @@ func (f *File) AddVBAProject(bin string) error { Type: SourceRelationshipVBAProject, }) } - file, _ := os.ReadFile(filepath.Clean(bin)) f.Pkg.Store("xl/vbaProject.bin", file) return err } diff --git a/excelize_test.go b/excelize_test.go index e09c4b81ea..6ff1fc4e39 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1319,8 +1319,8 @@ func TestProtectSheet(t *testing.T) { })) ws, err = f.workSheetReader(sheetName) assert.NoError(t, err) - assert.Equal(t, 24, len(ws.SheetProtection.SaltValue)) - assert.Equal(t, 88, len(ws.SheetProtection.HashValue)) + assert.Len(t, ws.SheetProtection.SaltValue, 24) + assert.Len(t, ws.SheetProtection.HashValue, 88) assert.Equal(t, int(sheetProtectionSpinCount), ws.SheetProtection.SpinCount) // Test remove sheet protection with an incorrect password assert.EqualError(t, f.UnprotectSheet(sheetName, "wrongPassword"), ErrUnprotectSheetPassword.Error()) @@ -1387,8 +1387,8 @@ func TestProtectWorkbook(t *testing.T) { wb, err := f.workbookReader() assert.NoError(t, err) assert.Equal(t, "SHA-512", wb.WorkbookProtection.WorkbookAlgorithmName) - assert.Equal(t, 24, len(wb.WorkbookProtection.WorkbookSaltValue)) - assert.Equal(t, 88, len(wb.WorkbookProtection.WorkbookHashValue)) + assert.Len(t, wb.WorkbookProtection.WorkbookSaltValue, 24) + assert.Len(t, wb.WorkbookProtection.WorkbookHashValue, 88) assert.Equal(t, int(workbookProtectionSpinCount), wb.WorkbookProtection.WorkbookSpinCount) // Test protect workbook with password exceeds the limit length @@ -1447,18 +1447,21 @@ func TestSetDefaultTimeStyle(t *testing.T) { } func TestAddVBAProject(t *testing.T) { - f := NewFile() - assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) - assert.EqualError(t, f.AddVBAProject("macros.bin"), "stat macros.bin: no such file or directory") - assert.EqualError(t, f.AddVBAProject(filepath.Join("test", "Book1.xlsx")), ErrAddVBAProject.Error()) - assert.NoError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin"))) - // Test add VBA project twice - assert.NoError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin"))) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddVBAProject.xlsm"))) - // Test add VBA with unsupported charset workbook relationships - f.Relationships.Delete(defaultXMLPathWorkbookRels) - f.Pkg.Store(defaultXMLPathWorkbookRels, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin")), "XML syntax error on line 1: invalid UTF-8") + f := NewFile() + file, err := os.ReadFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) + assert.EqualError(t, f.AddVBAProject(file), ErrAddVBAProject.Error()) + file, err = os.ReadFile(filepath.Join("test", "vbaProject.bin")) + assert.NoError(t, err) + assert.NoError(t, f.AddVBAProject(file)) + // Test add VBA project twice + assert.NoError(t, f.AddVBAProject(file)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddVBAProject.xlsm"))) + // Test add VBA with unsupported charset workbook relationships + f.Relationships.Delete(defaultXMLPathWorkbookRels) + f.Pkg.Store(defaultXMLPathWorkbookRels, MacintoshCyrillicCharset) + assert.EqualError(t, f.AddVBAProject(file), "XML syntax error on line 1: invalid UTF-8") } func TestContentTypesReader(t *testing.T) { diff --git a/file.go b/file.go index 2896ee44fc..416c934332 100644 --- a/file.go +++ b/file.go @@ -39,12 +39,8 @@ func NewFile() *File { f.Pkg.Store(defaultXMLPathContentTypes, []byte(xml.Header+templateContentTypes)) f.SheetCount = 1 f.CalcChain, _ = f.calcChainReader() - f.Comments = make(map[string]*xlsxComments) f.ContentTypes, _ = f.contentTypesReader() - f.Drawings = sync.Map{} f.Styles, _ = f.stylesReader() - f.DecodeVMLDrawing = make(map[string]*decodeVmlDrawing) - f.VMLDrawing = make(map[string]*vmlDrawing) f.WorkBook, _ = f.workbookReader() f.Relationships = sync.Map{} rels, _ := f.relsReader(defaultXMLPathWorkbookRels) diff --git a/lib_test.go b/lib_test.go index ab0ccc9ae8..013cf05316 100644 --- a/lib_test.go +++ b/lib_test.go @@ -274,7 +274,7 @@ func TestBytesReplace(t *testing.T) { } func TestGetRootElement(t *testing.T) { - assert.Equal(t, 0, len(getRootElement(xml.NewDecoder(strings.NewReader(""))))) + assert.Len(t, getRootElement(xml.NewDecoder(strings.NewReader(""))), 0) } func TestSetIgnorableNameSpace(t *testing.T) { diff --git a/merge_test.go b/merge_test.go index 9bef612cb7..6c9d2025a9 100644 --- a/merge_test.go +++ b/merge_test.go @@ -93,7 +93,7 @@ func TestMergeCellOverlap(t *testing.T) { } mc, err := f.GetMergeCells("Sheet1") assert.NoError(t, err) - assert.Equal(t, 1, len(mc)) + assert.Len(t, mc, 1) assert.Equal(t, "A1", mc[0].GetStartAxis()) assert.Equal(t, "D3", mc[0].GetEndAxis()) assert.Equal(t, "", mc[0].GetCellValue()) diff --git a/shape.go b/shape.go index b0d449b281..cb8f49d5b4 100644 --- a/shape.go +++ b/shape.go @@ -56,7 +56,7 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { // &excelize.Shape{ // Type: "rect", // Line: excelize.ShapeLine{Color: "4286F4", Width: &lineWidth}, -// Fill: excelize.Fill{Color: []string{"8EB9FF"}}, +// Fill: excelize.Fill{Color: []string{"8EB9FF"}, Pattern: 1}, // Paragraph: []excelize.RichTextRun{ // { // Text: "Rectangle Shape", diff --git a/sheet_test.go b/sheet_test.go index fae4e596a3..cfed8fdb90 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -273,7 +273,7 @@ func TestDefinedName(t *testing.T) { Name: "Amount", })) assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[0].RefersTo) - assert.Exactly(t, 1, len(f.GetDefinedName())) + assert.Len(t, f.GetDefinedName(), 1) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx"))) // Test set defined name with unsupported charset workbook f.WorkBook = nil @@ -376,7 +376,7 @@ func TestGetSheetMap(t *testing.T) { for idx, name := range sheetMap { assert.Equal(t, expectedMap[idx], name) } - assert.Equal(t, len(sheetMap), 2) + assert.Len(t, sheetMap, 2) assert.NoError(t, f.Close()) f = NewFile() diff --git a/stream.go b/stream.go index 0577a7c918..55a2268dd9 100644 --- a/stream.go +++ b/stream.go @@ -139,12 +139,13 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { // AddTable creates an Excel table for the StreamWriter using the given // cell range and format set. For example, create a table of A1:D5: // -// err := sw.AddTable("A1:D5", nil) +// err := sw.AddTable(&excelize.Table{Range: "A1:D5"}) // // Create a table of F2:H6 with format set: // // disable := false -// err := sw.AddTable("F2:H6", &excelize.TableOptions{ +// err := sw.AddTable(&excelize.Table{ +// Range: "F2:H6", // Name: "table", // StyleName: "TableStyleMedium2", // ShowFirstColumn: true, @@ -160,12 +161,12 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { // called after the rows are written but before Flush. // // See File.AddTable for details on the table format. -func (sw *StreamWriter) AddTable(rangeRef string, opts *TableOptions) error { - options, err := parseTableOptions(opts) +func (sw *StreamWriter) AddTable(table *Table) error { + options, err := parseTableOptions(table) if err != nil { return err } - coordinates, err := rangeRefToCoordinates(rangeRef) + coordinates, err := rangeRefToCoordinates(options.Range) if err != nil { return err } @@ -202,7 +203,7 @@ func (sw *StreamWriter) AddTable(rangeRef string, opts *TableOptions) error { name = "Table" + strconv.Itoa(tableID) } - table := xlsxTable{ + tbl := xlsxTable{ XMLNS: NameSpaceSpreadSheet.Value, ID: tableID, Name: name, @@ -237,7 +238,7 @@ func (sw *StreamWriter) AddTable(rangeRef string, opts *TableOptions) error { if err = sw.file.addContentTypePart(tableID, "table"); err != nil { return err } - b, _ := xml.Marshal(table) + b, _ := xml.Marshal(tbl) sw.file.saveFileList(tableXML, b) return err } diff --git a/stream_test.go b/stream_test.go index da25bb9db7..720f59854d 100644 --- a/stream_test.go +++ b/stream_test.go @@ -196,7 +196,7 @@ func TestStreamTable(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) // Test add table without table header - assert.EqualError(t, streamWriter.AddTable("A1:C2", nil), "XML syntax error on line 2: unexpected EOF") + assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A1:C2"}), "XML syntax error on line 2: unexpected EOF") // Write some rows. We want enough rows to force a temp file (>16MB) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) row := []interface{}{1, 2, 3} @@ -205,7 +205,7 @@ func TestStreamTable(t *testing.T) { } // Write a table - assert.NoError(t, streamWriter.AddTable("A1:C2", nil)) + assert.NoError(t, streamWriter.AddTable(&Table{Range: "A1:C2"})) assert.NoError(t, streamWriter.Flush()) // Verify the table has names @@ -217,17 +217,17 @@ func TestStreamTable(t *testing.T) { assert.Equal(t, "B", table.TableColumns.TableColumn[1].Name) assert.Equal(t, "C", table.TableColumns.TableColumn[2].Name) - assert.NoError(t, streamWriter.AddTable("A1:C1", nil)) + assert.NoError(t, streamWriter.AddTable(&Table{Range: "A1:C1"})) // Test add table with illegal cell reference - assert.EqualError(t, streamWriter.AddTable("A:B1", nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, streamWriter.AddTable("A1:B", nil), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A1:B"}), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) // Test add table with invalid table name - assert.EqualError(t, streamWriter.AddTable("A:B1", &TableOptions{Name: "1Table"}), newInvalidTableNameError("1Table").Error()) + assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"}), newInvalidTableNameError("1Table").Error()) // Test add table with unsupported charset content types file.ContentTypes = nil file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, streamWriter.AddTable("A1:C2", nil), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A1:C2"}), "XML syntax error on line 1: invalid UTF-8") } func TestStreamMergeCells(t *testing.T) { diff --git a/table.go b/table.go index 60cfb9ae93..a914e15d55 100644 --- a/table.go +++ b/table.go @@ -23,10 +23,10 @@ import ( // parseTableOptions provides a function to parse the format settings of the // table with default value. -func parseTableOptions(opts *TableOptions) (*TableOptions, error) { +func parseTableOptions(opts *Table) (*Table, error) { var err error if opts == nil { - return &TableOptions{ShowRowStripes: boolPtr(true)}, err + return &Table{ShowRowStripes: boolPtr(true)}, err } if opts.ShowRowStripes == nil { opts.ShowRowStripes = boolPtr(true) @@ -41,12 +41,13 @@ func parseTableOptions(opts *TableOptions) (*TableOptions, error) { // name, range reference and format set. For example, create a table of A1:D5 // on Sheet1: // -// err := f.AddTable("Sheet1", "A1:D5", nil) +// err := f.AddTable("Sheet1", &excelize.Table{Range: "A1:D5"}) // // Create a table of F2:H6 on Sheet2 with format set: // // disable := false -// err := f.AddTable("Sheet2", "F2:H6", &excelize.TableOptions{ +// err := f.AddTable("Sheet2", &excelize.Table{ +// Range: "F2:H6", // Name: "table", // StyleName: "TableStyleMedium2", // ShowFirstColumn: true, @@ -69,13 +70,13 @@ func parseTableOptions(opts *TableOptions) (*TableOptions, error) { // TableStyleLight1 - TableStyleLight21 // TableStyleMedium1 - TableStyleMedium28 // TableStyleDark1 - TableStyleDark11 -func (f *File) AddTable(sheet, rangeRef string, opts *TableOptions) error { - options, err := parseTableOptions(opts) +func (f *File) AddTable(sheet string, table *Table) error { + options, err := parseTableOptions(table) if err != nil { return err } // Coordinate conversion, convert C1:B3 to 2,0,1,2. - coordinates, err := rangeRefToCoordinates(rangeRef) + coordinates, err := rangeRefToCoordinates(options.Range) if err != nil { return err } @@ -187,7 +188,7 @@ func checkTableName(name string) error { // addTable provides a function to add table by given worksheet name, // range reference and format set. -func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *TableOptions) error { +func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Table) error { // Correct the minimum number of rows, the table at least two lines. if y1 == y2 { y2++ diff --git a/table_test.go b/table_test.go index f55a5a0ce7..046a933890 100644 --- a/table_test.go +++ b/table_test.go @@ -12,8 +12,9 @@ import ( func TestAddTable(t *testing.T) { f, err := prepareTestBook1() assert.NoError(t, err) - assert.NoError(t, f.AddTable("Sheet1", "B26:A21", nil)) - assert.NoError(t, f.AddTable("Sheet2", "A2:B5", &TableOptions{ + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "B26:A21"})) + assert.NoError(t, f.AddTable("Sheet2", &Table{ + Range: "A2:B5", Name: "table", StyleName: "TableStyleMedium2", ShowColumnStripes: true, @@ -21,21 +22,24 @@ func TestAddTable(t *testing.T) { ShowLastColumn: true, ShowRowStripes: boolPtr(true), })) - assert.NoError(t, f.AddTable("Sheet2", "D1:D11", &TableOptions{ + assert.NoError(t, f.AddTable("Sheet2", &Table{ + Range: "D1:D11", ShowHeaderRow: boolPtr(false), })) - assert.NoError(t, f.AddTable("Sheet2", "F1:F1", &TableOptions{StyleName: "TableStyleMedium8"})) + assert.NoError(t, f.AddTable("Sheet2", &Table{Range: "F1:F1", StyleName: "TableStyleMedium8"})) + // Test add table with invalid table options + assert.Equal(t, f.AddTable("Sheet1", nil), ErrParameterInvalid) // Test add table in not exist worksheet - assert.EqualError(t, f.AddTable("SheetN", "B26:A21", nil), "sheet SheetN does not exist") + assert.EqualError(t, f.AddTable("SheetN", &Table{Range: "B26:A21"}), "sheet SheetN does not exist") // Test add table with illegal cell reference - assert.EqualError(t, f.AddTable("Sheet1", "A:B1", nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, f.AddTable("Sheet1", "A1:B", nil), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + assert.Equal(t, f.AddTable("Sheet1", &Table{Range: "A:B1"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) + assert.Equal(t, f.AddTable("Sheet1", &Table{Range: "A1:B"}), newCellNameToCoordinatesError("B", newInvalidCellNameError("B"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) // Test add table with invalid sheet name - assert.EqualError(t, f.AddTable("Sheet:1", "B26:A21", nil), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddTable("Sheet:1", &Table{Range: "B26:A21"}), ErrSheetNameInvalid.Error()) // Test addTable with illegal cell reference f = NewFile() assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell reference [0, 0]") @@ -54,8 +58,9 @@ func TestAddTable(t *testing.T) { {name: "\u0f5f\u0fb3\u0f0b\u0f21", err: newInvalidTableNameError("\u0f5f\u0fb3\u0f0b\u0f21")}, {name: strings.Repeat("c", MaxFieldLength+1), err: ErrTableNameLength}, } { - assert.EqualError(t, f.AddTable("Sheet1", "A1:B2", &TableOptions{ - Name: cases.name, + assert.EqualError(t, f.AddTable("Sheet1", &Table{ + Range: "A1:B2", + Name: cases.name, }), cases.err.Error()) } } diff --git a/test/vbaProject.bin b/test/vbaProject.bin old mode 100755 new mode 100644 index fc15dca28e5ffd30e75f7464d4306110caaf3ff9..7e88db07c1f26ef2d4c082543627c13a90d546f6 GIT binary patch delta 5323 zcmZu#3vg7`89w*!O|rY$4GV-M5Wq_wQF z_#jK66&)(Vu@(DpKx;*-t%9{$Ya6B0+8Jx5wX`~|Lg}<+IxY4wQ|nCGe*e99A;z3L z=br!k|9}41Ip;s;-t2xMJNc|x6iT^8tQN|-ODuOSV2mw9U4(i$m&=u*tuWxlXqTW? zqAn%ebDIwdN2D&Hn6HqoDLM9+7lNy{z9X1G$~yTS(rxqdD5K^7k~Z5QG*<`~YWZQQ zUfeA4I|@4OtO!WURYFZ6+46ZS|EM6u+eEWO=I{-LZcR(cwcy zZPIN7oG9vPA+{_!LYctmVQsMPk3cv?-GVUs50S+vAhY{gFP!Br^UC?R{9+(}-v`Gw z9qFETSN*q+yt$YU+NvFAUU+4T{H1i{WcwqQt&hL-#;JSE`vl<_FSfSwJtd768>@wo z&mEKbb0u}fdJiV#61>;Co;Q}-9Hgj&Nz|}WX;eDAxon;psulFTEz5Qkv-i2Mu8o%$ zU*yN6iX%2l%^wjNve^%t>&%R`pav*XD7wwNPji=TrK8Ba;Nr`tw??16V76TQr~a?- z0b7lu{I4}Xy!7VT?&rVT_n+FI6urvtG1tknk)Ig(p6zX+k)N_366FGJv%05j^H+*5 zYAQ_$YRD@p1XCG;aif<<*cf2KPoghrem*FYl@0sqMZ()aeLx$**ip3m4gO_JkJj)$ zr^e?1U$LOer(wA)glGchyZQN&Wjl_dKbc#_J~RZ}B??T$Be4gynVzYzg2H6*%~nV< z$uXJ$g2jEbXmU9-dN&IEU`dUwMze^}2L%3ulBzgF?LA0DMN&UY}=F{*m~5N@@4e1hM<}9 z&A?M3ev)4!Dl;@Pgord;&bz&!={*@`~|zdQ%OL)wN7I|rd2g2=@gmTCqa1O5X{*KGWaz@M7~KgL%r zT;KU3s87#9eGB-n&4K?5@CWC>Gyd|zRf}O(t^>=CmZZK-^-wB&UTL}TBHoot6QC=` z(j;m=FJ#3@5qUv2Gl|Kd>v=&~z#3VcUC&gs)y&P3{I1f9`C;b8qq0HP%vzX-4f4lJ zd!`Cm)!tH8q9s&F-fNcp>>zUhl=hlol8J2~jafa7kg<9Go*6s|z(f;-F+kshIrMy4 zqP?rUoX4)BauI zp}pa+UT-OdQCAALuUpMK%Il{@mN1YswXjl4d4$qqX<|R4lMF>mLwLVKOehxQZ7QnK zlAC#6Gfq?wXg3+Oq{b;=;xs@D3GwInpUZ3c!3v*UJ7;NaY1qS3jkO(}ql97na(R@W zu273Npz}S#0GF%1y>KId*KPccm7R9v)~f{vaZULYi1Q3v89eJmw9)bvj+Vm-|3Z;H z#L9IP>o-U@FF7T4-pU=7pQ2{0vWF{@gHKn=f{CB6thA6cQ^L$9dBIXIdIEY9yNgGb zwpG88JV`OiNpR7Ey>!B)3B?FP7a+J!{#eaTf}8)ZW@?^~HDQs**$phjqRhwdsogBB zhKFZ(%<4J%BubBG~byGYYN%pa`BIVBJC*;Rhzl?$)wJ6Kp_uP{V8uG;D#+6m*XR z4uY?nb+YSv@Y(}s2sMT$!jgz!68$b1Isj-Ca32Ql1T_u=@-q^LfLdR+4c?-?+`KGA-(77d!&o_yQgWbi?>zPtyh0KN}Tp|kE6>S0FQov=XOSMz@>WxFs zidWaL4hdMUs|sjW0Fo9K+}A3y3Z9b|SC+#0;EZJHfrUx%tq?W(P%d}jthV^{Bl7E5 zyL5!~vLP_05C)Zzh)?8OaMg+*=gZ}GpVmcH(0*Kp%wSk23r;7yMD z!-)gYfk&PUlIv zjXNe1tWfIHGV0T@1S`nGUX#GI#QLs{hLk_fls(JTJkl+SpufTWH&7l9_G2w{L&tNP^iuj@3?tlil3WTv?M~h=rB|+eAs5 z8Hypr3O70|qFl~O7q+I#C8CIqzNVIoCcvV0JPNFa)(!t#^8(a`JYlXg3B{+l$C`_$6aaL0C8Vi2EZg z3VR}JDZF=@1uOi4m_NXpJt0?%&yTF+@kN?l{%AH9a77})SVx=e3Hc+@1Kur2HJR~v zFw%`2)44~D2gi_Gk`u{zR~kuWG~SomIW-nfG{THQQy$;ct29l2ratx?5T?BlD_Qp@bqYZC~Y2L+oy#z z8)1!0-Yj;%TfB4{UzXMlAwd-{&@L}q9ukFuR_7la7tU5Ks*+Yc$`*faymu(olbTT1 z|H0mSY<}v~*(4Hx>t^JKGgM#hrrB)Ksm2B z?DmT#8{ho(FGCNFovt;1^!tl~cFxjydjf8ww89|T8Mwz-X}4T0L=~Wy%D($jSMD~Q zx-Re@sSDE;qwpX&dJFj-b+}TH0pt?Np(};$o9Ixba%Am9!INm$^4)7UnXQ2GY|2yU z{MOJ$)*LrHv0TxH4@iSvzycz6Xqk8uZYF4kC9Qe5Kx=jvQd0+BEeYTyl0h$hEBp5={?=^4@x?J`Ef7`#J0INk@2eGT{nRI$0zG=sf z;enLu<*q*?+?`Zz%3SH*!aXCZp38T zbH+dtYRJ+S1qt5_$EiHaKM17q>8PtKFGM$vguH4n>O%Cnx@h3`Qn*p3wRiqSA)S5V zW1L98k-}@@IIjlNpB<0)?jG;o)bI7gW8G73g(6GJ={jwk38eiWhK78bcVjVmijp+i zxAd;D=g5MM{KI?q_y21B{r~;_cm7`b;(H|!?gbYtpvl)0l#!oq9?Rz^Jw1)Zwd8~x ztsPEH$WLU<8AI7@WBxnKm~%6X5&7{oi<~ffij0!Yx&zZXM~rZ@f6wrLt~n(1^5d1) z;GfVUKHa(j!!eVVn~dqqe>UiIfm`+r+Icf?Ym4AwZsysxHW#VsjI>wXiDMuZRU@h^ zZyfxK+XD}Ryxnlvv74`dkE>#Uj zU4fX_?^2Xt5F>n$5xbioZ@+G1*d12XmKIMW+~S{EtL)ZDPpG}h-{SYRxC4=h*Q-W+ ydIfELxV_5b_XMFv2`W@0F}}9rP`M}Il&Pu#)q}U<@jB(L_-dOUyu{_;OaBLS>tQ1R delta 5266 zcmcIodvKK16~EuN-)=VhNH!0WO*RQ%5-<-E*jF|iFpz8>Bp4tN1SwQ@fyKmx1e0wG zDYy$klu8H4ZG}#qimeZ*s0eDYf+qNA8DF4uYHk03>S)okbF;G;V@2R>a1Qu-JRaw_846OT zYnAKIEE^F2vl!TjG`jnDs`phI(miw{e8rY9gx@-L)i7CoAcU|5UugHpM8 zRmX?*?N%XG1GR=31{;7hGyjJ^V4<-yP|XDMHp7K%0@_e%2UGbLYN$%_ONy7>DL1Qy zqbZ#=Q8dNCLXN zlCBf&B+L1WriJ`xHX}`k2|PX9fttlEFhA-mPuSN9ry9*=0*_fzXKciLKLYH>K#!yR z9QZYwE=V0P;knQt_f+P2ob_t(?rFI)Gn`%KR0 zyt14hGM34c!O30u?bb^|IrpUP7mw(9fyu2eWEM7)cbTfWY%>aWzSmTSl0j$AsN_4- z3!|6e%GUTSb{SsTnIRk$1*XHF#2!T8)M?8IQ}IZZNFlHw5Q_WIr}}P4B4r%No)KD$ z8$@kXlc@kQXUrB4DWqF*6Geb;+gH^w6$)}k)qkFf_u8Y_A1`%ou306BT= z(1=s<(S{MRCJNEmF|GV>e9KatBSj)|u3X>;Y(C)v-*4-U{srF1yVcomg#vAc7`VQq zYakgv0X@PmfD_S}ENOrfXix5UTI&Z`iPQzu69jc#`~Z1VDIV5A8}J8{@b@+^NTGOb0lqH@r%)1pAMovp)wi>3nIhdr zOxvC)VdL4dbA-cmvlMg#pnHXgR2mq~49g&{6%Q`Uk&UQdK!bAcFd>swvbAhEb3v9e zH*@pt+4hWitcGm@=4b1f6W=g@G`lmJ!b)};nSrG;nJl90XZz5ObBD5s^IuMQOueoU z%y>fIHVIGY+kumwAZ*b}otl&(ACS~DKL8z6R|6%gip$WIj>U5HSfc zYnV>KsuV36lKPWGlos&uX&0k`Z7V~4OkNQhydxZuS>uqrJk)}u_s&M%k;*cyrahFNl8JAbTgn@9%cD9*u^^&#rUMwG3P_Es$c|w*5GhwJ zz8AIn*P1LgsO>bpgszdo2?wYo5ujmA)Cehjw0E!owsLSIbeIvI>YZ z!4j6MoPSubT5$6E!l>DY%Vi}CuomXyZxk*Q3V2~r*=SzGa3|CU%zQYno%WH14*G0m zYcV$JY_bX|0L2qA~OHf2Sg4t>Z`BGqLo;do*(v-E4ONzBgUQeIv*E=%`>tF?yt&~qBE zpHXOEv_~vLGR2=_CFCGuk-2<{Cmh25&WZK*`6}Mu_Geu||6HK|$@65&>iYewKTS;-! zYNveskR%Y5EZ1OZvOI&d$EW2+Z-9>GocA$XnXo#(1O$d@(dpB zU;N0>tb)guoXY;*6XPxY*`!8C;`P+fLkmBCed$X}?#_MSv)f7%_`5GWkoIHa+krEy zUU>8Bws)UN;6KVd`_rp;E)G0bdgR_i&#!nrfs>>>8B<&>5DHnGd68)KmX@tZOFW~j z)TWabhc=u#8Dx^QGSKLIlCLi7i5e`pUU0iy%?~oI?D-UBqtrRr(;w-I4EBx(afVk7 z4E7F@=J@p>GhrCK9^>l*>%JQrT-WuT^=C)Xoh=@^BQUV_&~oZd9o7#F^!D0=OfM-} z@&ikd<=wEf4mpok*ILppdeK=@bL#O{K2|ofRL!Q>4hLDfGM2!U1gYjn%D5l)^Ht^3 z)Ad>_(rn*Q{tx*d${neFh(V#KV;MMhSt$&MG9DMPNTr@MB!47z<%niN1BVLBmZmLS z9yHUK13HxUDfJ4hjfW~~`JReQaF$M}Jjq|FsJDrOLbg$F$V?S8)&H5{{>UKfW$VX;fia;n|82AT19QO`{4%u#1lPH3w5xN9 z0-~U=I#;}CPqbuOz&I-)TC?dCTEB?^s0QBggRe^A4L3xD|nzMEsV@dvMhORWCY$ zuC{!7ej#Kk(^vT)7S*|HPVO9fcMRT23Ne%tEHQRBpa~4kyZiXgIgV%rnFG1EpRRQL zNo+a$+UWfIQ>=RhC96<&If@_<(IUs=xS8>gpi+XUjV>>ukspM=c^=hK?f{XV$PUso z2K2QiXsGy95e4yhgHHO;A9j1M8lBD z6l&m=-eSJaTQ(2YqY3YaA{+7kB}evZdTNN(KF??vJqn0A`B`r`adJ935=K;C$jCNL zQ8l7g9@elr+ESpP@=biusC%E%F=$NrvR1+-|DZvbtmqX@RGj=v2tVW7j~}uwI_7Yv zl+I(dZC<)MRKk2xUD~7)&t_~(5dGM3qwS)Be_7ki8~itysjHp*5{9+{dXhj7c1dAH zdvkrs_gh?bt^OuwOLbFYZJ^rgbGxhkPPeDJwyCzZ&g=I2JuOZAm_Oia9@y}m&Eft? zy*k94o7$WjSxpn$JV|XluWm_3YjbO>yRpXQ@ijMj8o{Hki2m1btebChEj3zDq6y;% zwi$*qs8~h3#ur(%C@2CzWFj!0?VL}x6fS`eoq_rizt``p@iaHqH2a)2Eh?jJVN-pH zGvKUiZt~Uo+&({XT{Ayfx4*$%QzaAoJ#|&Gx6W6E|39fStanz)-~4ORP|su(j(zb@ Dcn_v1 diff --git a/xmlTable.go b/xmlTable.go index 0779a8e004..789d4a2873 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -196,8 +196,9 @@ type xlsxTableStyleInfo struct { ShowColumnStripes bool `xml:"showColumnStripes,attr"` } -// TableOptions directly maps the format settings of the table. -type TableOptions struct { +// Table directly maps the format settings of the table. +type Table struct { + Range string Name string StyleName string ShowColumnStripes bool From 9dbba9f34ab452e65341a4450cc650506177cceb Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 28 Mar 2023 00:05:18 +0800 Subject: [PATCH 176/213] This closes #1508, support SST index which contains blank characters --- cell.go | 2 +- cell_test.go | 5 +++++ stream.go | 30 +++++++++++++++++------------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/cell.go b/cell.go index 5ebcefee6c..5fcadcedf0 100644 --- a/cell.go +++ b/cell.go @@ -564,7 +564,7 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { case "s": if c.V != "" { xlsxSI := 0 - xlsxSI, _ = strconv.Atoi(c.V) + xlsxSI, _ = strconv.Atoi(strings.TrimSpace(c.V)) if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw) } diff --git a/cell_test.go b/cell_test.go index 58a4bee62a..565c1c93b8 100644 --- a/cell_test.go +++ b/cell_test.go @@ -432,6 +432,11 @@ func TestGetValueFrom(t *testing.T) { value, err := c.getValueFrom(f, sst, false) assert.NoError(t, err) assert.Equal(t, "", value) + + c = xlsxC{T: "s", V: " 1 "} + value, err = c.getValueFrom(f, &xlsxSST{Count: 1, SI: []xlsxSI{{}, {T: &xlsxT{Val: "s"}}}}, false) + assert.NoError(t, err) + assert.Equal(t, "s", value) } func TestGetCellFormula(t *testing.T) { diff --git a/stream.go b/stream.go index 55a2268dd9..82d7129630 100644 --- a/stream.go +++ b/stream.go @@ -47,23 +47,23 @@ type StreamWriter struct { // 16MB. For example, set data for worksheet of size 102400 rows x 50 columns // with numbers and style: // -// file := excelize.NewFile() +// f := excelize.NewFile() // defer func() { -// if err := file.Close(); err != nil { +// if err := f.Close(); err != nil { // fmt.Println(err) // } // }() -// streamWriter, err := file.NewStreamWriter("Sheet1") +// sw, err := f.NewStreamWriter("Sheet1") // if err != nil { // fmt.Println(err) // return // } -// styleID, err := file.NewStyle(&excelize.Style{Font: &excelize.Font{Color: "777777"}}) +// styleID, err := f.NewStyle(&excelize.Style{Font: &excelize.Font{Color: "777777"}}) // if err != nil { // fmt.Println(err) // return // } -// if err := streamWriter.SetRow("A1", +// if err := sw.SetRow("A1", // []interface{}{ // excelize.Cell{StyleID: styleID, Value: "Data"}, // []excelize.RichTextRun{ @@ -80,30 +80,34 @@ type StreamWriter struct { // for colID := 0; colID < 50; colID++ { // row[colID] = rand.Intn(640000) // } -// cell, _ := excelize.CoordinatesToCellName(1, rowID) -// if err := streamWriter.SetRow(cell, row); err != nil { +// cell, err := excelize.CoordinatesToCellName(1, rowID) +// if err != nil { // fmt.Println(err) -// return +// break +// } +// if err := sw.SetRow(cell, row); err != nil { +// fmt.Println(err) +// break // } // } -// if err := streamWriter.Flush(); err != nil { +// if err := sw.Flush(); err != nil { // fmt.Println(err) // return // } -// if err := file.SaveAs("Book1.xlsx"); err != nil { +// if err := f.SaveAs("Book1.xlsx"); err != nil { // fmt.Println(err) // } // // Set cell value and cell formula for a worksheet with stream writer: // -// err := streamWriter.SetRow("A1", []interface{}{ +// err := sw.SetRow("A1", []interface{}{ // excelize.Cell{Value: 1}, // excelize.Cell{Value: 2}, // excelize.Cell{Formula: "SUM(A1,B1)"}}); // // Set cell value and rows style for a worksheet with stream writer: // -// err := streamWriter.SetRow("A1", []interface{}{ +// err := sw.SetRow("A1", []interface{}{ // excelize.Cell{Value: 1}}, // excelize.RowOpts{StyleID: styleID, Height: 20, Hidden: false}); func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { @@ -432,7 +436,7 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt // the 'SetColWidth' function before the 'SetRow' function. For example set // the width column B:C as 20: // -// err := streamWriter.SetColWidth(2, 3, 20) +// err := sw.SetColWidth(2, 3, 20) func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { if sw.sheetWritten { return ErrStreamSetColWidth From 3b807c4bfee5e42b84e60d7b8195f908a9f4269e Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 29 Mar 2023 00:00:27 +0800 Subject: [PATCH 177/213] This support get cell hyperlink for merged cells --- cell.go | 9 +++++---- datavalidation_test.go | 2 +- excelize_test.go | 32 ++++++++++++++++---------------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/cell.go b/cell.go index 5fcadcedf0..3f1e65293f 100644 --- a/cell.go +++ b/cell.go @@ -801,12 +801,13 @@ func (f *File) GetCellHyperLink(sheet, cell string) (bool, string, error) { if err != nil { return false, "", err } - if cell, err = f.mergeCellsParser(ws, cell); err != nil { - return false, "", err - } if ws.Hyperlinks != nil { for _, link := range ws.Hyperlinks.Hyperlink { - if link.Ref == cell { + ok, err := f.checkCellInRangeRef(cell, link.Ref) + if err != nil { + return false, "", err + } + if link.Ref == cell || ok { if link.RID != "" { return true, f.getSheetRelationshipsTargetByID(sheet, link.RID), err } diff --git a/datavalidation_test.go b/datavalidation_test.go index 6b705918fb..2f45fd9963 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -87,7 +87,7 @@ func TestDataValidation(t *testing.T) { dataValidations, err = f.GetDataValidations("Sheet1") assert.NoError(t, err) - assert.Len(t,dataValidations, 3) + assert.Len(t, dataValidations, 3) // Test get data validation on no exists worksheet _, err = f.GetDataValidations("SheetN") diff --git a/excelize_test.go b/excelize_test.go index 6ff1fc4e39..f7afccc1a1 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -480,7 +480,7 @@ func TestGetCellHyperLink(t *testing.T) { ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) - ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + ws.(*xlsxWorksheet).Hyperlinks = &xlsxHyperlinks{Hyperlink: []xlsxHyperlink{{Ref: "A:A"}}} link, target, err = f.GetCellHyperLink("Sheet1", "A1") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, link, false) @@ -1447,21 +1447,21 @@ func TestSetDefaultTimeStyle(t *testing.T) { } func TestAddVBAProject(t *testing.T) { - f := NewFile() - file, err := os.ReadFile(filepath.Join("test", "Book1.xlsx")) - assert.NoError(t, err) - assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) - assert.EqualError(t, f.AddVBAProject(file), ErrAddVBAProject.Error()) - file, err = os.ReadFile(filepath.Join("test", "vbaProject.bin")) - assert.NoError(t, err) - assert.NoError(t, f.AddVBAProject(file)) - // Test add VBA project twice - assert.NoError(t, f.AddVBAProject(file)) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddVBAProject.xlsm"))) - // Test add VBA with unsupported charset workbook relationships - f.Relationships.Delete(defaultXMLPathWorkbookRels) - f.Pkg.Store(defaultXMLPathWorkbookRels, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddVBAProject(file), "XML syntax error on line 1: invalid UTF-8") + f := NewFile() + file, err := os.ReadFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) + assert.EqualError(t, f.AddVBAProject(file), ErrAddVBAProject.Error()) + file, err = os.ReadFile(filepath.Join("test", "vbaProject.bin")) + assert.NoError(t, err) + assert.NoError(t, f.AddVBAProject(file)) + // Test add VBA project twice + assert.NoError(t, f.AddVBAProject(file)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddVBAProject.xlsm"))) + // Test add VBA with unsupported charset workbook relationships + f.Relationships.Delete(defaultXMLPathWorkbookRels) + f.Pkg.Store(defaultXMLPathWorkbookRels, MacintoshCyrillicCharset) + assert.EqualError(t, f.AddVBAProject(file), "XML syntax error on line 1: invalid UTF-8") } func TestContentTypesReader(t *testing.T) { From 294f2e1480b9bd66cb9d74e35cbbd657c977e726 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 1 Apr 2023 00:08:53 +0800 Subject: [PATCH 178/213] Require using ChartType enumeration value to specify the chart type - Update docs and unit tests --- README.md | 2 +- README_zh.md | 2 +- chart.go | 271 +++++++++++++++++++++++++------------------------- chart_test.go | 164 +++++++++++++++--------------- drawing.go | 116 ++++++++++----------- errors.go | 4 +- xmlChart.go | 2 +- 7 files changed, 282 insertions(+), 279 deletions(-) diff --git a/README.md b/README.md index 48ff150807..0177eafdb6 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ func main() { f.SetSheetRow("Sheet1", cell, &row) } if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ - Type: "col3DClustered", + Type: excelize.Col3DClustered, Series: []excelize.ChartSeries{ { Name: "Sheet1!$A$2", diff --git a/README_zh.md b/README_zh.md index b6c689bec5..c6ad9075e4 100644 --- a/README_zh.md +++ b/README_zh.md @@ -148,7 +148,7 @@ func main() { f.SetSheetRow("Sheet1", cell, &row) } if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ - Type: "col3DClustered", + Type: excelize.Col3DClustered, Series: []excelize.ChartSeries{ { Name: "Sheet1!$A$2", diff --git a/chart.go b/chart.go index a9b3aaae67..b4a73feb5a 100644 --- a/chart.go +++ b/chart.go @@ -18,68 +18,71 @@ import ( "strings" ) -// This section defines the currently supported chart types. +// ChartType is the type of supported chart types. +type ChartType byte + +// This section defines the currently supported chart types enumeration. const ( - Area = "area" - AreaStacked = "areaStacked" - AreaPercentStacked = "areaPercentStacked" - Area3D = "area3D" - Area3DStacked = "area3DStacked" - Area3DPercentStacked = "area3DPercentStacked" - Bar = "bar" - BarStacked = "barStacked" - BarPercentStacked = "barPercentStacked" - Bar3DClustered = "bar3DClustered" - Bar3DStacked = "bar3DStacked" - Bar3DPercentStacked = "bar3DPercentStacked" - Bar3DConeClustered = "bar3DConeClustered" - Bar3DConeStacked = "bar3DConeStacked" - Bar3DConePercentStacked = "bar3DConePercentStacked" - Bar3DPyramidClustered = "bar3DPyramidClustered" - Bar3DPyramidStacked = "bar3DPyramidStacked" - Bar3DPyramidPercentStacked = "bar3DPyramidPercentStacked" - Bar3DCylinderClustered = "bar3DCylinderClustered" - Bar3DCylinderStacked = "bar3DCylinderStacked" - Bar3DCylinderPercentStacked = "bar3DCylinderPercentStacked" - Col = "col" - ColStacked = "colStacked" - ColPercentStacked = "colPercentStacked" - Col3D = "col3D" - Col3DClustered = "col3DClustered" - Col3DStacked = "col3DStacked" - Col3DPercentStacked = "col3DPercentStacked" - Col3DCone = "col3DCone" - Col3DConeClustered = "col3DConeClustered" - Col3DConeStacked = "col3DConeStacked" - Col3DConePercentStacked = "col3DConePercentStacked" - Col3DPyramid = "col3DPyramid" - Col3DPyramidClustered = "col3DPyramidClustered" - Col3DPyramidStacked = "col3DPyramidStacked" - Col3DPyramidPercentStacked = "col3DPyramidPercentStacked" - Col3DCylinder = "col3DCylinder" - Col3DCylinderClustered = "col3DCylinderClustered" - Col3DCylinderStacked = "col3DCylinderStacked" - Col3DCylinderPercentStacked = "col3DCylinderPercentStacked" - Doughnut = "doughnut" - Line = "line" - Line3D = "line3D" - Pie = "pie" - Pie3D = "pie3D" - PieOfPieChart = "pieOfPie" - BarOfPieChart = "barOfPie" - Radar = "radar" - Scatter = "scatter" - Surface3D = "surface3D" - WireframeSurface3D = "wireframeSurface3D" - Contour = "contour" - WireframeContour = "wireframeContour" - Bubble = "bubble" - Bubble3D = "bubble3D" + Area ChartType = iota + AreaStacked + AreaPercentStacked + Area3D + Area3DStacked + Area3DPercentStacked + Bar + BarStacked + BarPercentStacked + Bar3DClustered + Bar3DStacked + Bar3DPercentStacked + Bar3DConeClustered + Bar3DConeStacked + Bar3DConePercentStacked + Bar3DPyramidClustered + Bar3DPyramidStacked + Bar3DPyramidPercentStacked + Bar3DCylinderClustered + Bar3DCylinderStacked + Bar3DCylinderPercentStacked + Col + ColStacked + ColPercentStacked + Col3D + Col3DClustered + Col3DStacked + Col3DPercentStacked + Col3DCone + Col3DConeClustered + Col3DConeStacked + Col3DConePercentStacked + Col3DPyramid + Col3DPyramidClustered + Col3DPyramidStacked + Col3DPyramidPercentStacked + Col3DCylinder + Col3DCylinderClustered + Col3DCylinderStacked + Col3DCylinderPercentStacked + Doughnut + Line + Line3D + Pie + Pie3D + PieOfPie + BarOfPie + Radar + Scatter + Surface3D + WireframeSurface3D + Contour + WireframeContour + Bubble + Bubble3D ) // This section defines the default value of chart properties. var ( - chartView3DRotX = map[string]int{ + chartView3DRotX = map[ChartType]int{ Area: 0, AreaStacked: 0, AreaPercentStacked: 0, @@ -125,8 +128,8 @@ var ( Line3D: 20, Pie: 0, Pie3D: 30, - PieOfPieChart: 0, - BarOfPieChart: 0, + PieOfPie: 0, + BarOfPie: 0, Radar: 0, Scatter: 0, Surface3D: 15, @@ -134,7 +137,7 @@ var ( Contour: 90, WireframeContour: 90, } - chartView3DRotY = map[string]int{ + chartView3DRotY = map[ChartType]int{ Area: 0, AreaStacked: 0, AreaPercentStacked: 0, @@ -180,8 +183,8 @@ var ( Line3D: 15, Pie: 0, Pie3D: 0, - PieOfPieChart: 0, - BarOfPieChart: 0, + PieOfPie: 0, + BarOfPie: 0, Radar: 0, Scatter: 0, Surface3D: 20, @@ -189,18 +192,18 @@ var ( Contour: 0, WireframeContour: 0, } - plotAreaChartOverlap = map[string]int{ + plotAreaChartOverlap = map[ChartType]int{ BarStacked: 100, BarPercentStacked: 100, ColStacked: 100, ColPercentStacked: 100, } - chartView3DPerspective = map[string]int{ + chartView3DPerspective = map[ChartType]int{ Line3D: 30, Contour: 0, WireframeContour: 0, } - chartView3DRAngAx = map[string]int{ + chartView3DRAngAx = map[ChartType]int{ Area: 0, AreaStacked: 0, AreaPercentStacked: 0, @@ -246,8 +249,8 @@ var ( Line3D: 0, Pie: 0, Pie3D: 0, - PieOfPieChart: 0, - BarOfPieChart: 0, + PieOfPie: 0, + BarOfPie: 0, Radar: 0, Scatter: 0, Surface3D: 0, @@ -263,7 +266,7 @@ var ( "top": "t", "top_right": "tr", } - chartValAxNumFmtFormatCode = map[string]string{ + chartValAxNumFmtFormatCode = map[ChartType]string{ Area: "General", AreaStacked: "General", AreaPercentStacked: "0%", @@ -309,8 +312,8 @@ var ( Line3D: "General", Pie: "General", Pie3D: "General", - PieOfPieChart: "General", - BarOfPieChart: "General", + PieOfPie: "General", + BarOfPie: "General", Radar: "General", Scatter: "General", Surface3D: "General", @@ -320,7 +323,7 @@ var ( Bubble: "General", Bubble3D: "General", } - chartValAxCrossBetween = map[string]string{ + chartValAxCrossBetween = map[ChartType]string{ Area: "midCat", AreaStacked: "midCat", AreaPercentStacked: "midCat", @@ -366,8 +369,8 @@ var ( Line3D: "between", Pie: "between", Pie3D: "between", - PieOfPieChart: "between", - BarOfPieChart: "between", + PieOfPie: "between", + BarOfPie: "between", Radar: "between", Scatter: "between", Surface3D: "midCat", @@ -377,7 +380,7 @@ var ( Bubble: "midCat", Bubble3D: "midCat", } - plotAreaChartGrouping = map[string]string{ + plotAreaChartGrouping = map[ChartType]string{ Area: "standard", AreaStacked: "stacked", AreaPercentStacked: "percentStacked", @@ -421,7 +424,7 @@ var ( Line: "standard", Line3D: "standard", } - plotAreaChartBarDir = map[string]string{ + plotAreaChartBarDir = map[ChartType]string{ Bar: "bar", BarStacked: "bar", BarPercentStacked: "bar", @@ -471,7 +474,7 @@ var ( true: "r", false: "l", } - valTickLblPos = map[string]string{ + valTickLblPos = map[ChartType]string{ Contour: "none", WireframeContour: "none", } @@ -548,7 +551,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // f.SetSheetRow("Sheet1", cell, &row) // } // if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ -// Type: "col3DClustered", +// Type: excelize.Col3DClustered, // Series: []excelize.ChartSeries{ // { // Name: "Sheet1!$A$2", @@ -592,63 +595,63 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // // The following shows the type of chart supported by excelize: // -// Type | Chart -// -----------------------------+------------------------------ -// area | 2D area chart -// areaStacked | 2D stacked area chart -// areaPercentStacked | 2D 100% stacked area chart -// area3D | 3D area chart -// area3DStacked | 3D stacked area chart -// area3DPercentStacked | 3D 100% stacked area chart -// bar | 2D clustered bar chart -// barStacked | 2D stacked bar chart -// barPercentStacked | 2D 100% stacked bar chart -// bar3DClustered | 3D clustered bar chart -// bar3DStacked | 3D stacked bar chart -// bar3DPercentStacked | 3D 100% stacked bar chart -// bar3DConeClustered | 3D cone clustered bar chart -// bar3DConeStacked | 3D cone stacked bar chart -// bar3DConePercentStacked | 3D cone percent bar chart -// bar3DPyramidClustered | 3D pyramid clustered bar chart -// bar3DPyramidStacked | 3D pyramid stacked bar chart -// bar3DPyramidPercentStacked | 3D pyramid percent stacked bar chart -// bar3DCylinderClustered | 3D cylinder clustered bar chart -// bar3DCylinderStacked | 3D cylinder stacked bar chart -// bar3DCylinderPercentStacked | 3D cylinder percent stacked bar chart -// col | 2D clustered column chart -// colStacked | 2D stacked column chart -// colPercentStacked | 2D 100% stacked column chart -// col3DClustered | 3D clustered column chart -// col3D | 3D column chart -// col3DStacked | 3D stacked column chart -// col3DPercentStacked | 3D 100% stacked column chart -// col3DCone | 3D cone column chart -// col3DConeClustered | 3D cone clustered column chart -// col3DConeStacked | 3D cone stacked column chart -// col3DConePercentStacked | 3D cone percent stacked column chart -// col3DPyramid | 3D pyramid column chart -// col3DPyramidClustered | 3D pyramid clustered column chart -// col3DPyramidStacked | 3D pyramid stacked column chart -// col3DPyramidPercentStacked | 3D pyramid percent stacked column chart -// col3DCylinder | 3D cylinder column chart -// col3DCylinderClustered | 3D cylinder clustered column chart -// col3DCylinderStacked | 3D cylinder stacked column chart -// col3DCylinderPercentStacked | 3D cylinder percent stacked column chart -// doughnut | doughnut chart -// line | line chart -// line3D | 3D line chart -// pie | pie chart -// pie3D | 3D pie chart -// pieOfPie | pie of pie chart -// barOfPie | bar of pie chart -// radar | radar chart -// scatter | scatter chart -// surface3D | 3D surface chart -// wireframeSurface3D | 3D wireframe surface chart -// contour | contour chart -// wireframeContour | wireframe contour chart -// bubble | bubble chart -// bubble3D | 3D bubble chart +// ID | Enumeration | Chart +// ----+-----------------------------+------------------------------ +// 0 | Area | 2D area chart +// 1 | AreaStacked | 2D stacked area chart +// 2 | AreaPercentStacked | 2D 100% stacked area chart +// 3 | Area3D | 3D area chart +// 4 | Area3DStacked | 3D stacked area chart +// 5 | Area3DPercentStacked | 3D 100% stacked area chart +// 6 | Bar | 2D clustered bar chart +// 7 | BarStacked | 2D stacked bar chart +// 8 | BarPercentStacked | 2D 100% stacked bar chart +// 9 | Bar3DClustered | 3D clustered bar chart +// 10 | Bar3DStacked | 3D stacked bar chart +// 11 | Bar3DPercentStacked | 3D 100% stacked bar chart +// 12 | Bar3DConeClustered | 3D cone clustered bar chart +// 13 | Bar3DConeStacked | 3D cone stacked bar chart +// 14 | Bar3DConePercentStacked | 3D cone percent bar chart +// 15 | Bar3DPyramidClustered | 3D pyramid clustered bar chart +// 16 | Bar3DPyramidStacked | 3D pyramid stacked bar chart +// 17 | Bar3DPyramidPercentStacked | 3D pyramid percent stacked bar chart +// 18 | Bar3DCylinderClustered | 3D cylinder clustered bar chart +// 19 | Bar3DCylinderStacked | 3D cylinder stacked bar chart +// 20 | Bar3DCylinderPercentStacked | 3D cylinder percent stacked bar chart +// 21 | Col | 2D clustered column chart +// 22 | ColStacked | 2D stacked column chart +// 23 | ColPercentStacked | 2D 100% stacked column chart +// 24 | Col3DClustered | 3D clustered column chart +// 25 | Col3D | 3D column chart +// 26 | Col3DStacked | 3D stacked column chart +// 27 | Col3DPercentStacked | 3D 100% stacked column chart +// 28 | Col3DCone | 3D cone column chart +// 29 | Col3DConeClustered | 3D cone clustered column chart +// 30 | Col3DConeStacked | 3D cone stacked column chart +// 31 | Col3DConePercentStacked | 3D cone percent stacked column chart +// 32 | Col3DPyramid | 3D pyramid column chart +// 33 | Col3DPyramidClustered | 3D pyramid clustered column chart +// 34 | Col3DPyramidStacked | 3D pyramid stacked column chart +// 35 | Col3DPyramidPercentStacked | 3D pyramid percent stacked column chart +// 36 | Col3DCylinder | 3D cylinder column chart +// 37 | Col3DCylinderClustered | 3D cylinder clustered column chart +// 38 | Col3DCylinderStacked | 3D cylinder stacked column chart +// 39 | Col3DCylinderPercentStacked | 3D cylinder percent stacked column chart +// 40 | Doughnut | doughnut chart +// 41 | Line | line chart +// 42 | Line3D | 3D line chart +// 43 | Pie | pie chart +// 44 | Pie3D | 3D pie chart +// 45 | PieOfPie | pie of pie chart +// 46 | BarOfPie | bar of pie chart +// 47 | Radar | radar chart +// 48 | Scatter | scatter chart +// 49 | Surface3D | 3D surface chart +// 50 | WireframeSurface3D | 3D wireframe surface chart +// 51 | Contour | contour chart +// 52 | WireframeContour | wireframe contour chart +// 53 | Bubble | bubble chart +// 54 | Bubble3D | 3D bubble chart // // In Excel a chart series is a collection of information that defines which // data is plotted such as values, axis labels and formatting. diff --git a/chart_test.go b/chart_test.go index accfc59052..98efa57441 100644 --- a/chart_test.go +++ b/chart_test.go @@ -42,7 +42,7 @@ func TestChartSize(t *testing.T) { } assert.NoError(t, f.AddChart("Sheet1", "E4", &Chart{ - Type: "col3DClustered", + Type: Col3DClustered, Dimension: ChartDimension{ Width: 640, Height: 480, @@ -200,106 +200,106 @@ func TestAddChart(t *testing.T) { sheetName, cell string opts *Chart }{ - {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: "col", Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "000000"}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}}}}, - {sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: "colStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: "colPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: "col3DClustered", Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Clustered Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: "col3DStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: "col3DPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "X45", opts: &Chart{Type: "radar", Series: series, Format: format, Legend: ChartLegend{Position: "top_right", ShowLegendKey: false}, Title: ChartTitle{Name: "Radar Chart"}, PlotArea: plotArea, ShowBlanksAs: "span"}}, - {sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: "col3DConeStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "AF16", opts: &Chart{Type: "col3DConeClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "AF30", opts: &Chart{Type: "col3DConePercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "AF45", opts: &Chart{Type: "col3DCone", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "AN1", opts: &Chart{Type: "col3DPyramidStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "AN16", opts: &Chart{Type: "col3DPyramidClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "AN30", opts: &Chart{Type: "col3DPyramidPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "AN45", opts: &Chart{Type: "col3DPyramid", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "AV1", opts: &Chart{Type: "col3DCylinderStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "AV16", opts: &Chart{Type: "col3DCylinderClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: "col3DCylinderPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "AV45", opts: &Chart{Type: "col3DCylinder", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: "col3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: "line3D", Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}}}, - {sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: "scatter", Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Scatter Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: "doughnut", Series: series3, Format: format, Legend: ChartLegend{Position: "right", ShowLegendKey: false}, Title: ChartTitle{Name: "Doughnut Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}}, - {sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: "line", Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}}, - {sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: "pie3D", Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: "pie", Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Pie Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false, NumFmt: ChartNumFmt{CustomNumFmt: "0.00%;0;;"}}, ShowBlanksAs: "gap"}}, + {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "000000"}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}}}}, + {sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: ColStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Clustered Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: Col3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "X45", opts: &Chart{Type: Radar, Series: series, Format: format, Legend: ChartLegend{Position: "top_right", ShowLegendKey: false}, Title: ChartTitle{Name: "Radar Chart"}, PlotArea: plotArea, ShowBlanksAs: "span"}}, + {sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: Col3DConeStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AF16", opts: &Chart{Type: Col3DConeClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AF30", opts: &Chart{Type: Col3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AF45", opts: &Chart{Type: Col3DCone, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AN1", opts: &Chart{Type: Col3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AN16", opts: &Chart{Type: Col3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AN30", opts: &Chart{Type: Col3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AN45", opts: &Chart{Type: Col3DPyramid, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AV1", opts: &Chart{Type: Col3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AV16", opts: &Chart{Type: Col3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: Col3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AV45", opts: &Chart{Type: Col3DCylinder, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: Line3D, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}}}, + {sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: Scatter, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Scatter Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: Doughnut, Series: series3, Format: format, Legend: ChartLegend{Position: "right", ShowLegendKey: false}, Title: ChartTitle{Name: "Doughnut Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}}, + {sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: Line, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}}, + {sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: Pie3D, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: Pie, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Pie Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false, NumFmt: ChartNumFmt{CustomNumFmt: "0.00%;0;;"}}, ShowBlanksAs: "gap"}}, // bar series chart - {sheetName: "Sheet2", cell: "P48", opts: &Chart{Type: "bar", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "X48", opts: &Chart{Type: "barStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "P64", opts: &Chart{Type: "barPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked 100% Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "X64", opts: &Chart{Type: "bar3DClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "P80", opts: &Chart{Type: "bar3DStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{Maximum: &maximum, Minimum: &minimum}}}, - {sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: "bar3DPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}}, + {sheetName: "Sheet2", cell: "P48", opts: &Chart{Type: Bar, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "X48", opts: &Chart{Type: BarStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "P64", opts: &Chart{Type: BarPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked 100% Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "X64", opts: &Chart{Type: Bar3DClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "P80", opts: &Chart{Type: Bar3DStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{Maximum: &maximum, Minimum: &minimum}}}, + {sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: Bar3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}}, // area series chart - {sheetName: "Sheet2", cell: "AF1", opts: &Chart{Type: "area", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AN1", opts: &Chart{Type: "areaStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AF16", opts: &Chart{Type: "areaPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D 100% Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AN16", opts: &Chart{Type: "area3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AF32", opts: &Chart{Type: "area3DStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AN32", opts: &Chart{Type: "area3DPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF1", opts: &Chart{Type: Area, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN1", opts: &Chart{Type: AreaStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF16", opts: &Chart{Type: AreaPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D 100% Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN16", opts: &Chart{Type: Area3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF32", opts: &Chart{Type: Area3DStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN32", opts: &Chart{Type: Area3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, // cylinder series chart - {sheetName: "Sheet2", cell: "AF48", opts: &Chart{Type: "bar3DCylinderStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AF64", opts: &Chart{Type: "bar3DCylinderClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AF80", opts: &Chart{Type: "bar3DCylinderPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF48", opts: &Chart{Type: Bar3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF64", opts: &Chart{Type: Bar3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF80", opts: &Chart{Type: Bar3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, // cone series chart - {sheetName: "Sheet2", cell: "AN48", opts: &Chart{Type: "bar3DConeStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AN64", opts: &Chart{Type: "bar3DConeClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AN80", opts: &Chart{Type: "bar3DConePercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AV48", opts: &Chart{Type: "bar3DPyramidStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AV64", opts: &Chart{Type: "bar3DPyramidClustered", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "AV80", opts: &Chart{Type: "bar3DPyramidPercentStacked", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN48", opts: &Chart{Type: Bar3DConeStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN64", opts: &Chart{Type: Bar3DConeClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN80", opts: &Chart{Type: Bar3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AV48", opts: &Chart{Type: Bar3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AV64", opts: &Chart{Type: Bar3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AV80", opts: &Chart{Type: Bar3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, // surface series chart - {sheetName: "Sheet2", cell: "AV1", opts: &Chart{Type: "surface3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Surface Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}}, - {sheetName: "Sheet2", cell: "AV16", opts: &Chart{Type: "wireframeSurface3D", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Wireframe Surface Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}}, - {sheetName: "Sheet2", cell: "AV32", opts: &Chart{Type: "contour", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "BD1", opts: &Chart{Type: "wireframeContour", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Wireframe Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AV1", opts: &Chart{Type: Surface3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Surface Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}}, + {sheetName: "Sheet2", cell: "AV16", opts: &Chart{Type: WireframeSurface3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Wireframe Surface Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}}, + {sheetName: "Sheet2", cell: "AV32", opts: &Chart{Type: Contour, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "BD1", opts: &Chart{Type: WireframeContour, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Wireframe Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, // bubble chart - {sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: "bubble", Series: series4, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: "bubble3D", Series: series4, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, + {sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: Bubble, Series: series4, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: Bubble3D, Series: series4, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, // pie of pie chart - {sheetName: "Sheet2", cell: "BD48", opts: &Chart{Type: "pieOfPie", Series: series3, Format: format, Legend: legend, Title: ChartTitle{Name: "Pie of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, + {sheetName: "Sheet2", cell: "BD48", opts: &Chart{Type: PieOfPie, Series: series3, Format: format, Legend: legend, Title: ChartTitle{Name: "Pie of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, // bar of pie chart - {sheetName: "Sheet2", cell: "BD64", opts: &Chart{Type: "barOfPie", Series: series3, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, + {sheetName: "Sheet2", cell: "BD64", opts: &Chart{Type: BarOfPie, Series: series3, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, } { assert.NoError(t, f.AddChart(c.sheetName, c.cell, c.opts)) } // combo chart _, err = f.NewSheet("Combo Charts") assert.NoError(t, err) - clusteredColumnCombo := [][]string{ - {"A1", "line", "Clustered Column - Line Chart"}, - {"I1", "doughnut", "Clustered Column - Doughnut Chart"}, + clusteredColumnCombo := [][]interface{}{ + {"A1", Line, "Clustered Column - Line Chart"}, + {"I1", Doughnut, "Clustered Column - Doughnut Chart"}, } for _, props := range clusteredColumnCombo { - assert.NoError(t, f.AddChart("Combo Charts", props[0], &Chart{Type: "col", Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[2]}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1], Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) + assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[2].(string)}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) } - stackedAreaCombo := map[string][]string{ - "A16": {"line", "Stacked Area - Line Chart"}, - "I16": {"doughnut", "Stacked Area - Doughnut Chart"}, + stackedAreaCombo := map[string][]interface{}{ + "A16": {Line, "Stacked Area - Line Chart"}, + "I16": {Doughnut, "Stacked Area - Doughnut Chart"}, } for axis, props := range stackedAreaCombo { - assert.NoError(t, f.AddChart("Combo Charts", axis, &Chart{Type: "areaStacked", Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[1]}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[0], Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) + assert.NoError(t, f.AddChart("Combo Charts", axis, &Chart{Type: AreaStacked, Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[1].(string)}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[0].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) // Test with invalid sheet name - assert.EqualError(t, f.AddChart("Sheet:1", "A1", &Chart{Type: "col", Series: series[:1]}), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddChart("Sheet:1", "A1", &Chart{Type: Col, Series: series[:1]}), ErrSheetNameInvalid.Error()) // Test with illegal cell reference - assert.EqualError(t, f.AddChart("Sheet2", "A", &Chart{Type: "col", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.AddChart("Sheet2", "A", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // Test with unsupported chart type - assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: "unknown", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}), "unsupported chart type unknown") + assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: 0x37, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newUnsupportedChartType(0x37).Error()) // Test add combo chart with invalid format set - assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: "col", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}, nil), ErrParameterInvalid.Error()) + assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}, nil), ErrParameterInvalid.Error()) // Test add combo chart with unsupported chart type - assert.EqualError(t, f.AddChart("Sheet2", "BD64", &Chart{Type: "barOfPie", Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}, &Chart{Type: "unknown", Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}), "unsupported chart type unknown") + assert.EqualError(t, f.AddChart("Sheet2", "BD64", &Chart{Type: BarOfPie, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}, &Chart{Type: 0x37, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}), newUnsupportedChartType(0x37).Error()) assert.NoError(t, f.Close()) // Test add chart with unsupported charset content types. f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddChart("Sheet1", "P1", &Chart{Type: "col", Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: ChartTitle{Name: "2D Column Chart"}}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: ChartTitle{Name: "2D Column Chart"}}), "XML syntax error on line 1: invalid UTF-8") } func TestAddChartSheet(t *testing.T) { @@ -317,7 +317,7 @@ func TestAddChartSheet(t *testing.T) { {Name: "Sheet1!$A$3", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$3:$D$3"}, {Name: "Sheet1!$A$4", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4"}, } - assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: "col3DClustered", Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}})) + assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Col3DClustered, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}})) // Test set the chartsheet as active sheet var sheetIdx int for idx, sheetName := range f.GetSheetList() { @@ -332,11 +332,11 @@ func TestAddChartSheet(t *testing.T) { assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is not a worksheet") // Test add chartsheet on already existing name sheet - assert.EqualError(t, f.AddChartSheet("Sheet1", &Chart{Type: "col3DClustered", Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), ErrExistsSheet.Error()) + assert.EqualError(t, f.AddChartSheet("Sheet1", &Chart{Type: Col3DClustered, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), ErrExistsSheet.Error()) // Test add chartsheet with invalid sheet name - assert.EqualError(t, f.AddChartSheet("Sheet:1", nil, &Chart{Type: "col3DClustered", Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddChartSheet("Sheet:1", nil, &Chart{Type: Col3DClustered, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), ErrSheetNameInvalid.Error()) // Test with unsupported chart type - assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: "unknown", Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), "unsupported chart type unknown") + assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: 0x37, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), newUnsupportedChartType(0x37).Error()) assert.NoError(t, f.UpdateLinkedValue()) @@ -345,7 +345,7 @@ func TestAddChartSheet(t *testing.T) { f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddChartSheet("Chart4", &Chart{Type: "col", Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: ChartTitle{Name: "2D Column Chart"}}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddChartSheet("Chart4", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: ChartTitle{Name: "2D Column Chart"}}), "XML syntax error on line 1: invalid UTF-8") } func TestDeleteChart(t *testing.T) { @@ -380,7 +380,7 @@ func TestDeleteChart(t *testing.T) { ShowSerName: true, ShowVal: true, } - assert.NoError(t, f.AddChart("Sheet1", "P1", &Chart{Type: "col", Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"})) + assert.NoError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"})) assert.NoError(t, f.DeleteChart("Sheet1", "P1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteChart.xlsx"))) // Test delete chart with invalid sheet name @@ -429,12 +429,12 @@ func TestChartWithLogarithmicBase(t *testing.T) { cell string opts *Chart }{ - {cell: "C1", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart without log scaling"}}}, - {cell: "M1", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart with log 10.5 scaling"}, YAxis: ChartAxis{LogBase: 10.5}}}, - {cell: "A25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1.9 scaling"}, YAxis: ChartAxis{LogBase: 1.9}}}, - {cell: "F25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 2 scaling"}, YAxis: ChartAxis{LogBase: 2}}}, - {cell: "K25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000.1 scaling"}, YAxis: ChartAxis{LogBase: 1000.1}}}, - {cell: "P25", opts: &Chart{Type: "line", Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000 scaling"}, YAxis: ChartAxis{LogBase: 1000}}}, + {cell: "C1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart without log scaling"}}}, + {cell: "M1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart with log 10.5 scaling"}, YAxis: ChartAxis{LogBase: 10.5}}}, + {cell: "A25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1.9 scaling"}, YAxis: ChartAxis{LogBase: 1.9}}}, + {cell: "F25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 2 scaling"}, YAxis: ChartAxis{LogBase: 2}}}, + {cell: "K25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000.1 scaling"}, YAxis: ChartAxis{LogBase: 1000.1}}}, + {cell: "P25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000 scaling"}, YAxis: ChartAxis{LogBase: 1000}}}, } { // Add two chart, one without and one with log scaling assert.NoError(t, f.AddChart(sheet1, c.cell, c.opts)) diff --git a/drawing.go b/drawing.go index f035d50a5f..f04dc336ab 100644 --- a/drawing.go +++ b/drawing.go @@ -180,7 +180,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { }, }, } - plotAreaFunc := map[string]func(*Chart) *cPlotArea{ + plotAreaFunc := map[ChartType]func(*Chart) *cPlotArea{ Area: f.drawBaseChart, AreaStacked: f.drawBaseChart, AreaPercentStacked: f.drawBaseChart, @@ -226,8 +226,8 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { Line3D: f.drawLine3DChart, Pie: f.drawPieChart, Pie3D: f.drawPie3DChart, - PieOfPieChart: f.drawPieOfPieChart, - BarOfPieChart: f.drawBarOfPieChart, + PieOfPie: f.drawPieOfPieChart, + BarOfPie: f.drawBarOfPieChart, Radar: f.drawRadarChart, Scatter: f.drawScatterChart, Surface3D: f.drawSurface3DChart, @@ -293,213 +293,213 @@ func (f *File) drawBaseChart(opts *Chart) *cPlotArea { } catAx := f.drawPlotAreaCatAx(opts) valAx := f.drawPlotAreaValAx(opts) - charts := map[string]*cPlotArea{ - "area": { + charts := map[ChartType]*cPlotArea{ + Area: { AreaChart: &c, CatAx: catAx, ValAx: valAx, }, - "areaStacked": { + AreaStacked: { AreaChart: &c, CatAx: catAx, ValAx: valAx, }, - "areaPercentStacked": { + AreaPercentStacked: { AreaChart: &c, CatAx: catAx, ValAx: valAx, }, - "area3D": { + Area3D: { Area3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "area3DStacked": { + Area3DStacked: { Area3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "area3DPercentStacked": { + Area3DPercentStacked: { Area3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar": { + Bar: { BarChart: &c, CatAx: catAx, ValAx: valAx, }, - "barStacked": { + BarStacked: { BarChart: &c, CatAx: catAx, ValAx: valAx, }, - "barPercentStacked": { + BarPercentStacked: { BarChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DClustered": { + Bar3DClustered: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DStacked": { + Bar3DStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DPercentStacked": { + Bar3DPercentStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DConeClustered": { + Bar3DConeClustered: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DConeStacked": { + Bar3DConeStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DConePercentStacked": { + Bar3DConePercentStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DPyramidClustered": { + Bar3DPyramidClustered: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DPyramidStacked": { + Bar3DPyramidStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DPyramidPercentStacked": { + Bar3DPyramidPercentStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DCylinderClustered": { + Bar3DCylinderClustered: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DCylinderStacked": { + Bar3DCylinderStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bar3DCylinderPercentStacked": { + Bar3DCylinderPercentStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col": { + Col: { BarChart: &c, CatAx: catAx, ValAx: valAx, }, - "colStacked": { + ColStacked: { BarChart: &c, CatAx: catAx, ValAx: valAx, }, - "colPercentStacked": { + ColPercentStacked: { BarChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3D": { + Col3D: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DClustered": { + Col3DClustered: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DStacked": { + Col3DStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DPercentStacked": { + Col3DPercentStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DCone": { + Col3DCone: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DConeClustered": { + Col3DConeClustered: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DConeStacked": { + Col3DConeStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DConePercentStacked": { + Col3DConePercentStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DPyramid": { + Col3DPyramid: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DPyramidClustered": { + Col3DPyramidClustered: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DPyramidStacked": { + Col3DPyramidStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DPyramidPercentStacked": { + Col3DPyramidPercentStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DCylinder": { + Col3DCylinder: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DCylinderClustered": { + Col3DCylinderClustered: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DCylinderStacked": { + Col3DCylinderStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3DCylinderPercentStacked": { + Col3DCylinderPercentStacked: { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "bubble": { + Bubble: { BubbleChart: &c, CatAx: catAx, ValAx: valAx, }, - "bubble3D": { + Bubble3D: { BubbleChart: &c, CatAx: catAx, ValAx: valAx, @@ -756,7 +756,7 @@ func (f *File) drawBubbleChart(opts *Chart) *cPlotArea { // drawChartShape provides a function to draw the c:shape element by given // format sets. func (f *File) drawChartShape(opts *Chart) *attrValString { - shapes := map[string]string{ + shapes := map[ChartType]string{ Bar3DConeClustered: "cone", Bar3DConeStacked: "cone", Bar3DConePercentStacked: "cone", @@ -844,7 +844,7 @@ func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr { }, }, } - if chartSeriesSpPr, ok := map[string]*cSpPr{ + if chartSeriesSpPr, ok := map[ChartType]*cSpPr{ Line: spPrLine, Scatter: spPrScatter, }[opts.Type]; ok { return chartSeriesSpPr @@ -880,7 +880,7 @@ func (f *File) drawChartSeriesDPt(i int, opts *Chart) []*cDPt { }, }, }} - chartSeriesDPt := map[string][]*cDPt{Pie: dpt, Pie3D: dpt} + chartSeriesDPt := map[ChartType][]*cDPt{Pie: dpt, Pie3D: dpt} return chartSeriesDPt[opts.Type] } @@ -892,7 +892,7 @@ func (f *File) drawChartSeriesCat(v ChartSeries, opts *Chart) *cCat { F: v.Categories, }, } - chartSeriesCat := map[string]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil} + chartSeriesCat := map[ChartType]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil} if _, ok := chartSeriesCat[opts.Type]; ok || v.Categories == "" { return nil } @@ -907,7 +907,7 @@ func (f *File) drawChartSeriesVal(v ChartSeries, opts *Chart) *cVal { F: v.Values, }, } - chartSeriesVal := map[string]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil} + chartSeriesVal := map[ChartType]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil} if _, ok := chartSeriesVal[opts.Type]; ok { return nil } @@ -917,7 +917,7 @@ func (f *File) drawChartSeriesVal(v ChartSeries, opts *Chart) *cVal { // drawChartSeriesMarker provides a function to draw the c:marker element by // given data index and format sets. func (f *File) drawChartSeriesMarker(i int, opts *Chart) *cMarker { - defaultSymbol := map[string]*attrValString{Scatter: {Val: stringPtr("circle")}} + defaultSymbol := map[ChartType]*attrValString{Scatter: {Val: stringPtr("circle")}} marker := &cMarker{ Symbol: defaultSymbol[opts.Type], Size: &attrValInt{Val: intPtr(5)}, @@ -945,7 +945,7 @@ func (f *File) drawChartSeriesMarker(i int, opts *Chart) *cMarker { }, } } - chartSeriesMarker := map[string]*cMarker{Scatter: marker, Line: marker} + chartSeriesMarker := map[ChartType]*cMarker{Scatter: marker, Line: marker} return chartSeriesMarker[opts.Type] } @@ -957,7 +957,7 @@ func (f *File) drawChartSeriesXVal(v ChartSeries, opts *Chart) *cCat { F: v.Categories, }, } - chartSeriesXVal := map[string]*cCat{Scatter: cat, Bubble: cat, Bubble3D: cat} + chartSeriesXVal := map[ChartType]*cCat{Scatter: cat, Bubble: cat, Bubble3D: cat} return chartSeriesXVal[opts.Type] } @@ -969,14 +969,14 @@ func (f *File) drawChartSeriesYVal(v ChartSeries, opts *Chart) *cVal { F: v.Values, }, } - chartSeriesYVal := map[string]*cVal{Scatter: val, Bubble: val, Bubble3D: val} + chartSeriesYVal := map[ChartType]*cVal{Scatter: val, Bubble: val, Bubble3D: val} return chartSeriesYVal[opts.Type] } // drawCharSeriesBubbleSize provides a function to draw the c:bubbleSize // element by given chart series and format sets. func (f *File) drawCharSeriesBubbleSize(v ChartSeries, opts *Chart) *cVal { - if _, ok := map[string]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok || v.Sizes == "" { + if _, ok := map[ChartType]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok || v.Sizes == "" { return nil } return &cVal{ @@ -989,7 +989,7 @@ func (f *File) drawCharSeriesBubbleSize(v ChartSeries, opts *Chart) *cVal { // drawCharSeriesBubble3D provides a function to draw the c:bubble3D element // by given format sets. func (f *File) drawCharSeriesBubble3D(opts *Chart) *attrValBool { - if _, ok := map[string]bool{Bubble3D: true}[opts.Type]; !ok { + if _, ok := map[ChartType]bool{Bubble3D: true}[opts.Type]; !ok { return nil } return &attrValBool{Val: boolPtr(true)} @@ -1027,7 +1027,7 @@ func (f *File) drawChartDLbls(opts *Chart) *cDLbls { // given format sets. func (f *File) drawChartSeriesDLbls(opts *Chart) *cDLbls { dLbls := f.drawChartDLbls(opts) - chartSeriesDLbls := map[string]*cDLbls{ + chartSeriesDLbls := map[ChartType]*cDLbls{ Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil, } if _, ok := chartSeriesDLbls[opts.Type]; ok { diff --git a/errors.go b/errors.go index 2e86470afb..8da9daec19 100644 --- a/errors.go +++ b/errors.go @@ -48,8 +48,8 @@ func newInvalidTableNameError(name string) error { // newUnsupportedChartType defined the error message on receiving the chart // type are unsupported. -func newUnsupportedChartType(chartType string) error { - return fmt.Errorf("unsupported chart type %s", chartType) +func newUnsupportedChartType(chartType ChartType) error { + return fmt.Errorf("unsupported chart type %d", chartType) } // newUnzipSizeLimitError defined the error message on unzip size exceeds the diff --git a/xmlChart.go b/xmlChart.go index 56049af6f4..20b70517f9 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -561,7 +561,7 @@ type ChartPlotArea struct { // Chart directly maps the format settings of the chart. type Chart struct { - Type string + Type ChartType Series []ChartSeries Format GraphicOptions Dimension ChartDimension From 799317eac596e0b9a8bc6773fb9218e91b14b14c Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 10 Apr 2023 00:02:20 +0800 Subject: [PATCH 179/213] This upgrade dependencies package --- go.mod | 6 +++--- go.sum | 21 ++++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index a4a0a74daf..12b024e54a 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/stretchr/testify v1.8.0 github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 - golang.org/x/crypto v0.5.0 + golang.org/x/crypto v0.8.0 golang.org/x/image v0.5.0 - golang.org/x/net v0.7.0 - golang.org/x/text v0.7.0 + golang.org/x/net v0.9.0 + golang.org/x/text v0.9.0 ) require github.com/richardlehane/msoleps v1.0.3 // indirect diff --git a/go.sum b/go.sum index 7e2b95af21..3c5a9eb918 100644 --- a/go.sum +++ b/go.sum @@ -22,39 +22,42 @@ github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/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/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 4196348f9f53e1fe797a4a09951e43e822d78394 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 00:27:17 +0800 Subject: [PATCH 180/213] Upgrade actions/setup-go from 3 to 4 (#1512) --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 15c98857cf..4f26b1b9dc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} From 635ec33576b68a5264007ffc4a1027e0558a47da Mon Sep 17 00:00:00 2001 From: Valery Ozarnichuk Date: Wed, 12 Apr 2023 03:17:10 +0300 Subject: [PATCH 181/213] Support checking cell value length with multi-bytes characters (#1517) --- cell.go | 9 +++++---- cell_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/cell.go b/cell.go index 3f1e65293f..1f01ce36ba 100644 --- a/cell.go +++ b/cell.go @@ -20,6 +20,7 @@ import ( "strconv" "strings" "time" + "unicode/utf8" ) // CellType is the type of cell value type. @@ -397,8 +398,8 @@ func (f *File) SetCellStr(sheet, cell, value string) error { // setCellString provides a function to set string type to shared string // table. func (f *File) setCellString(value string) (t, v string, err error) { - if len(value) > TotalCellChars { - value = value[:TotalCellChars] + if utf8.RuneCountInString(value) > TotalCellChars { + value = string([]rune(value)[:TotalCellChars]) } t = "s" var si int @@ -458,8 +459,8 @@ func (f *File) setSharedString(val string) (int, error) { // trimCellValue provides a function to set string type to cell. func trimCellValue(value string) (v string, ns xml.Attr) { - if len(value) > TotalCellChars { - value = value[:TotalCellChars] + if utf8.RuneCountInString(value) > TotalCellChars { + value = string([]rune(value)[:TotalCellChars]) } if len(value) > 0 { prefix, suffix := value[0], value[len(value)-1] diff --git a/cell_test.go b/cell_test.go index 565c1c93b8..8f731b12ad 100644 --- a/cell_test.go +++ b/cell_test.go @@ -176,6 +176,19 @@ func TestSetCellFloat(t *testing.T) { assert.EqualError(t, f.SetCellFloat("Sheet:1", "A1", 123.42, -1, 64), ErrSheetNameInvalid.Error()) } +func TestSetCellValuesMultiByte(t *testing.T) { + value := strings.Repeat("\u042B", TotalCellChars+1) + + f := NewFile() + err := f.SetCellValue("Sheet1", "A1", value) + assert.NoError(t, err) + + v, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.NotEqual(t, value, v) + assert.Equal(t, TotalCellChars, len([]rune(v))) +} + func TestSetCellValue(t *testing.T) { f := NewFile() assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) From 17c029494ad5fab374256a69f5d760bde3c7b05e Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 16 Apr 2023 14:22:55 +0800 Subject: [PATCH 182/213] This closes #1519, escape XML characters after checking cell value length --- cell.go | 17 ++++++++++------- cell_test.go | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/cell.go b/cell.go index 1f01ce36ba..edfcb3c858 100644 --- a/cell.go +++ b/cell.go @@ -451,17 +451,22 @@ func (f *File) setSharedString(val string) (int, error) { sst.Count++ sst.UniqueCount++ t := xlsxT{Val: val} - val, t.Space = trimCellValue(val) + val, t.Space = trimCellValue(val, false) sst.SI = append(sst.SI, xlsxSI{T: &t}) f.sharedStringsMap[val] = sst.UniqueCount - 1 return sst.UniqueCount - 1, nil } // trimCellValue provides a function to set string type to cell. -func trimCellValue(value string) (v string, ns xml.Attr) { +func trimCellValue(value string, escape bool) (v string, ns xml.Attr) { if utf8.RuneCountInString(value) > TotalCellChars { value = string([]rune(value)[:TotalCellChars]) } + buf := &bytes.Buffer{} + if escape { + _ = xml.EscapeText(buf, []byte(value)) + value = buf.String() + } if len(value) > 0 { prefix, suffix := value[0], value[len(value)-1] for _, ascii := range []byte{9, 10, 13, 32} { @@ -492,15 +497,13 @@ func (c *xlsxC) setCellValue(val string) { // string. func (c *xlsxC) setInlineStr(val string) { c.T, c.V, c.IS = "inlineStr", "", &xlsxSI{T: &xlsxT{}} - buf := &bytes.Buffer{} - _ = xml.EscapeText(buf, []byte(val)) - c.IS.T.Val, c.IS.T.Space = trimCellValue(buf.String()) + c.IS.T.Val, c.IS.T.Space = trimCellValue(val, true) } // setStr set cell data type and value which containing a formula string. func (c *xlsxC) setStr(val string) { c.T, c.IS = "str", nil - c.V, c.XMLSpace = trimCellValue(val) + c.V, c.XMLSpace = trimCellValue(val, false) } // getCellDate parse cell value which containing a boolean. @@ -1031,7 +1034,7 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { return textRuns, ErrCellCharsLength } run := xlsxR{T: &xlsxT{}} - run.T.Val, run.T.Space = trimCellValue(textRun.Text) + run.T.Val, run.T.Space = trimCellValue(textRun.Text, false) fnt := textRun.Font if fnt != nil { run.RPr = newRpr(fnt) diff --git a/cell_test.go b/cell_test.go index 8f731b12ad..fdfd513282 100644 --- a/cell_test.go +++ b/cell_test.go @@ -177,16 +177,36 @@ func TestSetCellFloat(t *testing.T) { } func TestSetCellValuesMultiByte(t *testing.T) { - value := strings.Repeat("\u042B", TotalCellChars+1) - f := NewFile() - err := f.SetCellValue("Sheet1", "A1", value) - assert.NoError(t, err) - - v, err := f.GetCellValue("Sheet1", "A1") - assert.NoError(t, err) - assert.NotEqual(t, value, v) - assert.Equal(t, TotalCellChars, len([]rune(v))) + row := []interface{}{ + // Test set cell value with multi byte characters value + strings.Repeat("\u4E00", TotalCellChars+1), + // Test set cell value with XML escape characters + strings.Repeat("<>", TotalCellChars/2), + strings.Repeat(">", TotalCellChars-1), + strings.Repeat(">", TotalCellChars+1), + } + assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &row)) + // Test set cell value with XML escape characters in stream writer + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) + streamWriter, err := f.NewStreamWriter("Sheet2") + assert.NoError(t, err) + assert.NoError(t, streamWriter.SetRow("A1", row)) + assert.NoError(t, streamWriter.Flush()) + for _, sheetName := range []string{"Sheet1", "Sheet2"} { + for cell, expected := range map[string]int{ + "A1": TotalCellChars, + "B1": TotalCellChars - 1, + "C1": TotalCellChars - 1, + "D1": TotalCellChars, + } { + result, err := f.GetCellValue(sheetName, cell) + assert.NoError(t, err) + assert.Len(t, []rune(result), expected) + } + } + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellValuesMultiByte.xlsx"))) } func TestSetCellValue(t *testing.T) { From d0ad0f39ec04debb4e082fb74f361b61ada5a184 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 17 Apr 2023 08:48:30 +0800 Subject: [PATCH 183/213] This commit contains 5 changes: - Fix incorrect comment box size for multi-line plain text comments - Prevent create duplicate tables with the same name - Add new exported error variable `ErrExistsTableName` - Allocate buffer inside escape XML characters - Update the unit tests --- cell.go | 4 ++-- comment.go | 3 +++ errors.go | 2 ++ excelize_test.go | 2 +- table.go | 19 +++++++++++++++++++ table_test.go | 6 ++++++ 6 files changed, 33 insertions(+), 3 deletions(-) diff --git a/cell.go b/cell.go index edfcb3c858..da022cde8c 100644 --- a/cell.go +++ b/cell.go @@ -462,9 +462,9 @@ func trimCellValue(value string, escape bool) (v string, ns xml.Attr) { if utf8.RuneCountInString(value) > TotalCellChars { value = string([]rune(value)[:TotalCellChars]) } - buf := &bytes.Buffer{} if escape { - _ = xml.EscapeText(buf, []byte(value)) + var buf bytes.Buffer + _ = xml.EscapeText(&buf, []byte(value)) value = buf.String() } if len(value) > 0 { diff --git a/comment.go b/comment.go index 39f11762e8..25564cbded 100644 --- a/comment.go +++ b/comment.go @@ -126,6 +126,9 @@ func (f *File) AddComment(sheet string, comment Comment) error { } } } + if len(comment.Runs) == 0 { + rows, cols = 1, len(comment.Text) + } if err = f.addDrawingVML(commentID, drawingVML, comment.Cell, rows+1, cols); err != nil { return err } diff --git a/errors.go b/errors.go index 8da9daec19..7c7143c65c 100644 --- a/errors.go +++ b/errors.go @@ -239,6 +239,8 @@ var ( // ErrTableNameLength defined the error message on receiving the table name // length exceeds the limit. ErrTableNameLength = fmt.Errorf("the table name length exceeds the %d characters limit", MaxFieldLength) + // ErrExistsTableName defined the error message on given table already exists. + ErrExistsTableName = errors.New("the same name table already exists") // ErrCellStyles defined the error message on cell styles exceeds the limit. ErrCellStyles = fmt.Errorf("the cell styles exceeds the %d limit", MaxCellStyles) // ErrUnprotectWorkbook defined the error message on workbook has set no diff --git a/excelize_test.go b/excelize_test.go index f7afccc1a1..17d16f0d4b 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -773,7 +773,7 @@ func TestSetCellStyleNumberFormat(t *testing.T) { } assert.NoError(t, f.SetCellStyle("Sheet2", c, c, style)) cellValue, err := f.GetCellValue("Sheet2", c) - assert.Equal(t, expected[i][k], cellValue, "Sheet2!"+c, i, k) + assert.Equal(t, expected[i][k], cellValue, fmt.Sprintf("Sheet2!%s value: %s, number format: %d", c, value[i], k)) assert.NoError(t, err) } } diff --git a/table.go b/table.go index a914e15d55..386942724a 100644 --- a/table.go +++ b/table.go @@ -12,8 +12,10 @@ package excelize import ( + "bytes" "encoding/xml" "fmt" + "io" "regexp" "strconv" "strings" @@ -75,6 +77,23 @@ func (f *File) AddTable(sheet string, table *Table) error { if err != nil { return err } + var exist bool + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/tables/table") { + var t xlsxTable + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))). + Decode(&t); err != nil && err != io.EOF { + return true + } + if exist = t.Name == options.Name; exist { + return false + } + } + return true + }) + if exist { + return ErrExistsTableName + } // Coordinate conversion, convert C1:B3 to 2,0,1,2. coordinates, err := rangeRefToCoordinates(options.Range) if err != nil { diff --git a/table_test.go b/table_test.go index 046a933890..6eec13980d 100644 --- a/table_test.go +++ b/table_test.go @@ -28,6 +28,8 @@ func TestAddTable(t *testing.T) { })) assert.NoError(t, f.AddTable("Sheet2", &Table{Range: "F1:F1", StyleName: "TableStyleMedium8"})) + // Test add table with already exist table name + assert.Equal(t, f.AddTable("Sheet2", &Table{Name: "Table1"}), ErrExistsTableName) // Test add table with invalid table options assert.Equal(t, f.AddTable("Sheet1", nil), ErrParameterInvalid) // Test add table in not exist worksheet @@ -63,6 +65,10 @@ func TestAddTable(t *testing.T) { Name: cases.name, }), cases.err.Error()) } + // Test check duplicate table name with unsupported charset table parts + f = NewFile() + f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"})) } func TestSetTableHeader(t *testing.T) { From fb6ce60bd56f4ef80d9fda76dc8accb4bfdee4ff Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 19 Apr 2023 00:05:59 +0800 Subject: [PATCH 184/213] This closes #1523, preventing format text cell value as a numeric - Simplify variable declaration and error return statements - Remove the internal `xlsxTabColor` data type - Using the `xlsxColor` data type instead of `xlsxTabColor` - Update unit test, improve code coverage --- calc.go | 4 +- cell.go | 50 +++-- cell_test.go | 45 ++-- chart_test.go | 6 + crypt_test.go | 22 ++ excelize.go | 7 +- numfmt.go | 10 +- numfmt_test.go | 2 +- rows_test.go | 10 + sheetpr.go | 11 +- sparkline.go | 520 +++++++++++++++++++++++------------------------ styles.go | 34 ++-- xmlChartSheet.go | 8 +- xmlWorksheet.go | 25 +-- 14 files changed, 400 insertions(+), 354 deletions(-) diff --git a/calc.go b/calc.go index 47705caad1..faea6d743d 100644 --- a/calc.go +++ b/calc.go @@ -791,11 +791,11 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string result = token.Value() if isNum, precision, decimal := isNumeric(result); isNum { if precision > 15 { - result, err = f.formattedValue(styleIdx, strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64)), rawCellValue) + result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64))}, rawCellValue, CellTypeNumber) return } if !strings.HasPrefix(result, "0") { - result, err = f.formattedValue(styleIdx, strings.ToUpper(strconv.FormatFloat(decimal, 'f', -1, 64)), rawCellValue) + result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'f', -1, 64))}, rawCellValue, CellTypeNumber) } } return diff --git a/cell.go b/cell.go index da022cde8c..52d8186046 100644 --- a/cell.go +++ b/cell.go @@ -516,7 +516,7 @@ func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) { return "FALSE", nil } } - return f.formattedValue(c.S, c.V, raw) + return f.formattedValue(c, raw, CellTypeBool) } // setCellDefault prepares cell type and string type cell value by a given @@ -551,7 +551,7 @@ func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) { c.V = strconv.FormatFloat(excelTime, 'G', 15, 64) } } - return f.formattedValue(c.S, c.V, raw) + return f.formattedValue(c, raw, CellTypeBool) } // getValueFrom return a value from a column/row cell, this function is @@ -567,21 +567,20 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { return c.getCellDate(f, raw) case "s": if c.V != "" { - xlsxSI := 0 - xlsxSI, _ = strconv.Atoi(strings.TrimSpace(c.V)) + xlsxSI, _ := strconv.Atoi(strings.TrimSpace(c.V)) if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { - return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw) + return f.formattedValue(&xlsxC{S: c.S, V: f.getFromStringItem(xlsxSI)}, raw, CellTypeSharedString) } if len(d.SI) > xlsxSI { - return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw) + return f.formattedValue(&xlsxC{S: c.S, V: d.SI[xlsxSI].String()}, raw, CellTypeSharedString) } } - return f.formattedValue(c.S, c.V, raw) + return f.formattedValue(c, raw, CellTypeSharedString) case "inlineStr": if c.IS != nil { - return f.formattedValue(c.S, c.IS.String(), raw) + return f.formattedValue(&xlsxC{S: c.S, V: c.IS.String()}, raw, CellTypeInlineString) } - return f.formattedValue(c.S, c.V, raw) + return f.formattedValue(c, raw, CellTypeInlineString) default: if isNum, precision, decimal := isNumeric(c.V); isNum && !raw { if precision > 15 { @@ -590,7 +589,7 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { c.V = strconv.FormatFloat(decimal, 'f', -1, 64) } } - return f.formattedValue(c.S, c.V, raw) + return f.formattedValue(c, raw, CellTypeNumber) } } @@ -1325,47 +1324,44 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c // formattedValue provides a function to returns a value after formatted. If // it is possible to apply a format to the cell value, it will do so, if not // then an error will be returned, along with the raw value of the cell. -func (f *File) formattedValue(s int, v string, raw bool) (string, error) { - if raw { - return v, nil - } - if s == 0 { - return v, nil +func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, error) { + if raw || c.S == 0 { + return c.V, nil } styleSheet, err := f.stylesReader() if err != nil { - return v, err + return c.V, err } if styleSheet.CellXfs == nil { - return v, err + return c.V, err } - if s >= len(styleSheet.CellXfs.Xf) || s < 0 { - return v, err + if c.S >= len(styleSheet.CellXfs.Xf) || c.S < 0 { + return c.V, err } var numFmtID int - if styleSheet.CellXfs.Xf[s].NumFmtID != nil { - numFmtID = *styleSheet.CellXfs.Xf[s].NumFmtID + if styleSheet.CellXfs.Xf[c.S].NumFmtID != nil { + numFmtID = *styleSheet.CellXfs.Xf[c.S].NumFmtID } date1904 := false wb, err := f.workbookReader() if err != nil { - return v, err + return c.V, err } if wb != nil && wb.WorkbookPr != nil { date1904 = wb.WorkbookPr.Date1904 } if ok := builtInNumFmtFunc[numFmtID]; ok != nil { - return ok(v, builtInNumFmt[numFmtID], date1904), err + return ok(c.V, builtInNumFmt[numFmtID], date1904, cellType), err } if styleSheet.NumFmts == nil { - return v, err + return c.V, err } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { - return format(v, xlsxFmt.FormatCode, date1904), err + return format(c.V, xlsxFmt.FormatCode, date1904, cellType), err } } - return v, err + return c.V, err } // prepareCellStyle provides a function to prepare style index of cell in diff --git a/cell_test.go b/cell_test.go index fdfd513282..b395478805 100644 --- a/cell_test.go +++ b/cell_test.go @@ -803,21 +803,21 @@ func TestSetCellRichText(t *testing.T) { func TestFormattedValue(t *testing.T) { f := NewFile() - result, err := f.formattedValue(0, "43528", false) + result, err := f.formattedValue(&xlsxC{S: 0, V: "43528"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "43528", result) // S is too large - result, err = f.formattedValue(15, "43528", false) + result, err = f.formattedValue(&xlsxC{S: 15, V: "43528"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "43528", result) // S is too small - result, err = f.formattedValue(-15, "43528", false) + result, err = f.formattedValue(&xlsxC{S: -15, V: "43528"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "43528", result) - result, err = f.formattedValue(1, "43528", false) + result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "43528", result) customNumFmt := "[$-409]MM/DD/YYYY" @@ -825,7 +825,7 @@ func TestFormattedValue(t *testing.T) { CustomNumFmt: &customNumFmt, }) assert.NoError(t, err) - result, err = f.formattedValue(1, "43528", false) + result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "03/04/2019", result) @@ -834,7 +834,7 @@ func TestFormattedValue(t *testing.T) { f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: &numFmtID, }) - result, err = f.formattedValue(2, "43528", false) + result, err = f.formattedValue(&xlsxC{S: 2, V: "43528"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "43528", result) @@ -842,7 +842,7 @@ func TestFormattedValue(t *testing.T) { f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: nil, }) - result, err = f.formattedValue(3, "43528", false) + result, err = f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "43528", result) @@ -851,7 +851,16 @@ func TestFormattedValue(t *testing.T) { f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: &numFmtID, }) - result, err = f.formattedValue(1, "43528", false) + result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber) + assert.NoError(t, err) + assert.Equal(t, "43528", result) + + // Test format numeric value with shared string data type + f.Styles.NumFmts, numFmtID = nil, 11 + f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ + NumFmtID: &numFmtID, + }) + result, err = f.formattedValue(&xlsxC{S: 5, V: "43528"}, false, CellTypeSharedString) assert.NoError(t, err) assert.Equal(t, "43528", result) @@ -860,32 +869,36 @@ func TestFormattedValue(t *testing.T) { NumFmt: 1, }) assert.NoError(t, err) - result, err = f.formattedValue(styleID, "310.56", false) + result, err = f.formattedValue(&xlsxC{S: styleID, V: "310.56"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "311", result) for _, fn := range builtInNumFmtFunc { - assert.Equal(t, "0_0", fn("0_0", "", false)) + assert.Equal(t, "0_0", fn("0_0", "", false, CellTypeNumber)) } // Test format value with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) - _, err = f.formattedValue(1, "43528", false) + _, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") // Test format value with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) - _, err = f.formattedValue(1, "43528", false) + _, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + + for _, fn := range builtInNumFmtFunc { + assert.Equal(t, fn("text", "0", false, CellTypeNumber), "text") + } } func TestFormattedValueNilXfs(t *testing.T) { // Set the CellXfs to nil and verify that the formattedValue function does not crash f := NewFile() f.Styles.CellXfs = nil - result, err := f.formattedValue(3, "43528", false) + result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "43528", result) } @@ -894,7 +907,7 @@ func TestFormattedValueNilNumFmts(t *testing.T) { // Set the NumFmts value to nil and verify that the formattedValue function does not crash f := NewFile() f.Styles.NumFmts = nil - result, err := f.formattedValue(3, "43528", false) + result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "43528", result) } @@ -903,7 +916,7 @@ func TestFormattedValueNilWorkbook(t *testing.T) { // Set the Workbook value to nil and verify that the formattedValue function does not crash f := NewFile() f.WorkBook = nil - result, err := f.formattedValue(3, "43528", false) + result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "43528", result) } @@ -913,7 +926,7 @@ func TestFormattedValueNilWorkbookPr(t *testing.T) { // crash. f := NewFile() f.WorkBook.WorkbookPr = nil - result, err := f.formattedValue(3, "43528", false) + result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber) assert.NoError(t, err) assert.Equal(t, "43528", result) } diff --git a/chart_test.go b/chart_test.go index 98efa57441..f57cb4c837 100644 --- a/chart_test.go +++ b/chart_test.go @@ -121,6 +121,12 @@ func TestDeleteDrawing(t *testing.T) { path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) assert.EqualError(t, f.deleteDrawing(0, 0, path, "Chart"), "XML syntax error on line 1: invalid UTF-8") + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) + f.Drawings.Store(path, &xlsxWsDr{TwoCellAnchor: []*xdrCellAnchor{{ + GraphicFrame: string(MacintoshCyrillicCharset), + }}}) + assert.EqualError(t, f.deleteDrawing(0, 0, path, "Chart"), "XML syntax error on line 1: invalid UTF-8") } func TestAddChart(t *testing.T) { diff --git a/crypt_test.go b/crypt_test.go index dfbaaf3514..7b4cac7243 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -12,11 +12,14 @@ package excelize import ( + "bytes" + "encoding/binary" "os" "path/filepath" "strings" "testing" + "github.com/richardlehane/mscfb" "github.com/stretchr/testify/assert" ) @@ -51,6 +54,25 @@ func TestEncrypt(t *testing.T) { // Test remove password by save workbook with options assert.NoError(t, f.Save(Options{Password: ""})) assert.NoError(t, f.Close()) + + doc, err := mscfb.New(bytes.NewReader(raw)) + assert.NoError(t, err) + encryptionInfoBuf, encryptedPackageBuf := extractPart(doc) + binary.LittleEndian.PutUint64(encryptionInfoBuf[20:32], uint64(0)) + _, err = standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, &Options{Password: "password"}) + assert.NoError(t, err) + _, err = decrypt(nil, nil, nil) + assert.EqualError(t, err, "crypto/aes: invalid key size 0") + _, err = agileDecrypt(encryptionInfoBuf, MacintoshCyrillicCharset, &Options{Password: "password"}) + assert.EqualError(t, err, "XML syntax error on line 1: invalid character entity &0 (no semicolon)") + _, err = convertPasswdToKey("password", nil, Encryption{ + KeyEncryptors: KeyEncryptors{KeyEncryptor: []KeyEncryptor{ + {EncryptedKey: EncryptedKey{KeyData: KeyData{SaltValue: "=="}}}, + }}, + }) + assert.EqualError(t, err, "illegal base64 data at input byte 0") + _, err = createIV([]byte{0}, Encryption{KeyData: KeyData{SaltValue: "=="}}) + assert.EqualError(t, err, "illegal base64 data at input byte 0") } func TestEncryptionMechanism(t *testing.T) { diff --git a/excelize.go b/excelize.go index 51ae99b9cd..9903fbff5b 100644 --- a/excelize.go +++ b/excelize.go @@ -101,11 +101,10 @@ func OpenFile(filename string, opts ...Options) (*File, error) { } f, err := OpenReader(file, opts...) if err != nil { - closeErr := file.Close() - if closeErr == nil { - return f, err + if closeErr := file.Close(); closeErr != nil { + return f, closeErr } - return f, closeErr + return f, err } f.Path = filename return f, file.Close() diff --git a/numfmt.go b/numfmt.go index af95b54073..b5c81bf833 100644 --- a/numfmt.go +++ b/numfmt.go @@ -31,6 +31,7 @@ type languageInfo struct { // numberFormat directly maps the number format parser runtime required // fields. type numberFormat struct { + cellType CellType section []nfp.Section t time.Time sectionIdx int @@ -336,6 +337,9 @@ var ( // prepareNumberic split the number into two before and after parts by a // decimal point. func (nf *numberFormat) prepareNumberic(value string) { + if nf.cellType != CellTypeNumber && nf.cellType != CellTypeDate { + return + } if nf.isNumeric, _, _ = isNumeric(value); !nf.isNumeric { return } @@ -344,9 +348,9 @@ func (nf *numberFormat) prepareNumberic(value string) { // format provides a function to return a string parse by number format // expression. If the given number format is not supported, this will return // the original cell value. -func format(value, numFmt string, date1904 bool) string { +func format(value, numFmt string, date1904 bool, cellType CellType) string { p := nfp.NumberFormatParser() - nf := numberFormat{section: p.Parse(numFmt), value: value, date1904: date1904} + nf := numberFormat{section: p.Parse(numFmt), value: value, date1904: date1904, cellType: cellType} nf.number, nf.valueSectionType = nf.getValueSectionType(value) nf.prepareNumberic(value) for i, section := range nf.section { @@ -947,7 +951,7 @@ func (nf *numberFormat) textHandler() (result string) { if token.TType == nfp.TokenTypeLiteral { result += token.TValue } - if token.TType == nfp.TokenTypeTextPlaceHolder { + if token.TType == nfp.TokenTypeTextPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder { result += nf.value } } diff --git a/numfmt_test.go b/numfmt_test.go index 5540fb1de5..51ee8e21f0 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -1005,7 +1005,7 @@ func TestNumFmt(t *testing.T) { {"-8.0450685976001E-21", "0_);[Red]\\(0\\)", "(0)"}, {"-8.04506", "0_);[Red]\\(0\\)", "(8)"}, } { - result := format(item[0], item[1], false) + result := format(item[0], item[1], false, CellTypeNumber) assert.Equal(t, item[2], result, item) } } diff --git a/rows_test.go b/rows_test.go index 5de8d397e6..48a2735e57 100644 --- a/rows_test.go +++ b/rows_test.go @@ -11,6 +11,16 @@ import ( "github.com/stretchr/testify/require" ) +func TestGetRows(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetCellValue("Sheet1", "A1", "A1")) + // Test get rows with unsupported charset shared strings table + f.SharedStrings = nil + f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) + _, err := f.GetRows("Sheet1") + assert.NoError(t, err) +} + func TestRows(t *testing.T) { const sheet2 = "Sheet2" f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) diff --git a/sheetpr.go b/sheetpr.go index 41e7e98986..6b734e688f 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -116,7 +116,7 @@ func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) { prepareTabColor := func(ws *xlsxWorksheet) { ws.prepareSheetPr() if ws.SheetPr.TabColor == nil { - ws.SheetPr.TabColor = new(xlsxTabColor) + ws.SheetPr.TabColor = new(xlsxColor) } } if opts.CodeName != nil { @@ -145,7 +145,12 @@ func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) { if !s.Field(i).IsNil() { prepareTabColor(ws) name := s.Type().Field(i).Name - reflect.ValueOf(ws.SheetPr.TabColor).Elem().FieldByName(name[8:]).Set(s.Field(i).Elem()) + fld := reflect.ValueOf(ws.SheetPr.TabColor).Elem().FieldByName(name[8:]) + if s.Field(i).Kind() == reflect.Ptr && fld.Kind() == reflect.Ptr { + fld.Set(s.Field(i)) + continue + } + fld.Set(s.Field(i).Elem()) } } } @@ -206,7 +211,7 @@ func (f *File) GetSheetProps(sheet string) (SheetPropsOptions, error) { if ws.SheetPr.TabColor != nil { opts.TabColorIndexed = intPtr(ws.SheetPr.TabColor.Indexed) opts.TabColorRGB = stringPtr(ws.SheetPr.TabColor.RGB) - opts.TabColorTheme = intPtr(ws.SheetPr.TabColor.Theme) + opts.TabColorTheme = ws.SheetPr.TabColor.Theme opts.TabColorTint = float64Ptr(ws.SheetPr.TabColor.Tint) } } diff --git a/sparkline.go b/sparkline.go index 51bd106274..a208773844 100644 --- a/sparkline.go +++ b/sparkline.go @@ -23,337 +23,337 @@ import ( func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { groups := []*xlsxX14SparklineGroup{ { - ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, - ColorNegative: &xlsxTabColor{Theme: 5}, - ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, - ColorFirst: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, - ColorLast: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, - ColorHigh: &xlsxTabColor{Theme: 4}, - ColorLow: &xlsxTabColor{Theme: 4}, + ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262}, + ColorNegative: &xlsxColor{Theme: intPtr(5)}, + ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262}, + ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921}, + ColorLast: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921}, + ColorHigh: &xlsxColor{Theme: intPtr(4)}, + ColorLow: &xlsxColor{Theme: intPtr(4)}, }, // 0 { - ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, - ColorNegative: &xlsxTabColor{Theme: 5}, - ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, - ColorFirst: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, - ColorLast: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, - ColorHigh: &xlsxTabColor{Theme: 4}, - ColorLow: &xlsxTabColor{Theme: 4}, + ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262}, + ColorNegative: &xlsxColor{Theme: intPtr(5)}, + ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262}, + ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921}, + ColorLast: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921}, + ColorHigh: &xlsxColor{Theme: intPtr(4)}, + ColorLow: &xlsxColor{Theme: intPtr(4)}, }, // 1 { - ColorSeries: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, - ColorNegative: &xlsxTabColor{Theme: 6}, - ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, - ColorFirst: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921}, - ColorLast: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921}, - ColorHigh: &xlsxTabColor{Theme: 5}, - ColorLow: &xlsxTabColor{Theme: 5}, + ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262}, + ColorNegative: &xlsxColor{Theme: intPtr(6)}, + ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262}, + ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921}, + ColorLast: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921}, + ColorHigh: &xlsxColor{Theme: intPtr(5)}, + ColorLow: &xlsxColor{Theme: intPtr(5)}, }, // 2 { - ColorSeries: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, - ColorNegative: &xlsxTabColor{Theme: 7}, - ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, - ColorFirst: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921}, - ColorLast: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921}, - ColorHigh: &xlsxTabColor{Theme: 6}, - ColorLow: &xlsxTabColor{Theme: 6}, + ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262}, + ColorNegative: &xlsxColor{Theme: intPtr(7)}, + ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262}, + ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921}, + ColorLast: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921}, + ColorHigh: &xlsxColor{Theme: intPtr(6)}, + ColorLow: &xlsxColor{Theme: intPtr(6)}, }, // 3 { - ColorSeries: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, - ColorNegative: &xlsxTabColor{Theme: 8}, - ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, - ColorFirst: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921}, - ColorLast: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921}, - ColorHigh: &xlsxTabColor{Theme: 7}, - ColorLow: &xlsxTabColor{Theme: 7}, + ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262}, + ColorNegative: &xlsxColor{Theme: intPtr(8)}, + ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262}, + ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921}, + ColorLast: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921}, + ColorHigh: &xlsxColor{Theme: intPtr(7)}, + ColorLow: &xlsxColor{Theme: intPtr(7)}, }, // 4 { - ColorSeries: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, - ColorNegative: &xlsxTabColor{Theme: 9}, - ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, - ColorFirst: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921}, - ColorLast: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921}, - ColorHigh: &xlsxTabColor{Theme: 8}, - ColorLow: &xlsxTabColor{Theme: 8}, + ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262}, + ColorNegative: &xlsxColor{Theme: intPtr(9)}, + ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262}, + ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921}, + ColorLast: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921}, + ColorHigh: &xlsxColor{Theme: intPtr(8)}, + ColorLow: &xlsxColor{Theme: intPtr(8)}, }, // 5 { - ColorSeries: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, - ColorNegative: &xlsxTabColor{Theme: 4}, - ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, - ColorFirst: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921}, - ColorLast: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921}, - ColorHigh: &xlsxTabColor{Theme: 9}, - ColorLow: &xlsxTabColor{Theme: 9}, + ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262}, + ColorNegative: &xlsxColor{Theme: intPtr(4)}, + ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262}, + ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921}, + ColorLast: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921}, + ColorHigh: &xlsxColor{Theme: intPtr(9)}, + ColorLow: &xlsxColor{Theme: intPtr(9)}, }, // 6 { - ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, - ColorNegative: &xlsxTabColor{Theme: 5}, - ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 5}, - ColorLow: &xlsxTabColor{Theme: 5}, + ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, + ColorNegative: &xlsxColor{Theme: intPtr(5)}, + ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(5)}, + ColorLow: &xlsxColor{Theme: intPtr(5)}, }, // 7 { - ColorSeries: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, - ColorNegative: &xlsxTabColor{Theme: 6}, - ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893}, + ColorNegative: &xlsxColor{Theme: intPtr(6)}, + ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, }, // 8 { - ColorSeries: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, - ColorNegative: &xlsxTabColor{Theme: 7}, - ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, + ColorNegative: &xlsxColor{Theme: intPtr(7)}, + ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, }, // 9 { - ColorSeries: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, - ColorNegative: &xlsxTabColor{Theme: 8}, - ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, + ColorNegative: &xlsxColor{Theme: intPtr(8)}, + ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, }, // 10 { - ColorSeries: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, - ColorNegative: &xlsxTabColor{Theme: 9}, - ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, + ColorNegative: &xlsxColor{Theme: intPtr(9)}, + ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, }, // 11 { - ColorSeries: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, - ColorNegative: &xlsxTabColor{Theme: 4}, - ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, + ColorNegative: &xlsxColor{Theme: intPtr(4)}, + ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, }, // 12 { - ColorSeries: &xlsxTabColor{Theme: 4}, - ColorNegative: &xlsxTabColor{Theme: 5}, - ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(4)}, + ColorNegative: &xlsxColor{Theme: intPtr(5)}, + ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, }, // 13 { - ColorSeries: &xlsxTabColor{Theme: 5}, - ColorNegative: &xlsxTabColor{Theme: 6}, - ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(5)}, + ColorNegative: &xlsxColor{Theme: intPtr(6)}, + ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893}, }, // 14 { - ColorSeries: &xlsxTabColor{Theme: 6}, - ColorNegative: &xlsxTabColor{Theme: 7}, - ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(6)}, + ColorNegative: &xlsxColor{Theme: intPtr(7)}, + ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, }, // 15 { - ColorSeries: &xlsxTabColor{Theme: 7}, - ColorNegative: &xlsxTabColor{Theme: 8}, - ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(7)}, + ColorNegative: &xlsxColor{Theme: intPtr(8)}, + ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, }, // 16 { - ColorSeries: &xlsxTabColor{Theme: 8}, - ColorNegative: &xlsxTabColor{Theme: 9}, - ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(8)}, + ColorNegative: &xlsxColor{Theme: intPtr(9)}, + ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, }, // 17 { - ColorSeries: &xlsxTabColor{Theme: 9}, - ColorNegative: &xlsxTabColor{Theme: 4}, - ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(9)}, + ColorNegative: &xlsxColor{Theme: intPtr(4)}, + ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, }, // 18 { - ColorSeries: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, - ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, - ColorMarkers: &xlsxTabColor{Theme: 4, Tint: 0.79998168889431442}, - ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, - ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, + ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921}, + ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262}, + ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: 0.79998168889431442}, + ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262}, + ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262}, }, // 19 { - ColorSeries: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921}, - ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, - ColorMarkers: &xlsxTabColor{Theme: 5, Tint: 0.79998168889431442}, - ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, - ColorLow: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, + ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921}, + ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262}, + ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: 0.79998168889431442}, + ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262}, + ColorLow: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262}, }, // 20 { - ColorSeries: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921}, - ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, - ColorMarkers: &xlsxTabColor{Theme: 6, Tint: 0.79998168889431442}, - ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, - ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, + ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921}, + ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262}, + ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: 0.79998168889431442}, + ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262}, + ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262}, }, // 21 { - ColorSeries: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921}, - ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, - ColorMarkers: &xlsxTabColor{Theme: 7, Tint: 0.79998168889431442}, - ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, - ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, + ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921}, + ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262}, + ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: 0.79998168889431442}, + ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262}, + ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262}, }, // 22 { - ColorSeries: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921}, - ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, - ColorMarkers: &xlsxTabColor{Theme: 8, Tint: 0.79998168889431442}, - ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, - ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, + ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921}, + ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262}, + ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: 0.79998168889431442}, + ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262}, + ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262}, }, // 23 { - ColorSeries: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921}, - ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, - ColorMarkers: &xlsxTabColor{Theme: 9, Tint: 0.79998168889431442}, - ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, - ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, + ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921}, + ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262}, + ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: 0.79998168889431442}, + ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262}, + ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262}, }, // 24 { - ColorSeries: &xlsxTabColor{Theme: 1, Tint: 0.499984740745262}, - ColorNegative: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, - ColorMarkers: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(1), Tint: 0.499984740745262}, + ColorNegative: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893}, + ColorMarkers: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893}, }, // 25 { - ColorSeries: &xlsxTabColor{Theme: 1, Tint: 0.34998626667073579}, - ColorNegative: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, - ColorMarkers: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, - ColorFirst: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, - ColorLast: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, - ColorHigh: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, - ColorLow: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, + ColorSeries: &xlsxColor{Theme: intPtr(1), Tint: 0.34998626667073579}, + ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893}, + ColorMarkers: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893}, + ColorFirst: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893}, + ColorLast: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893}, + ColorHigh: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893}, + ColorLow: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893}, }, // 26 { - ColorSeries: &xlsxTabColor{RGB: "FF323232"}, - ColorNegative: &xlsxTabColor{RGB: "FFD00000"}, - ColorMarkers: &xlsxTabColor{RGB: "FFD00000"}, - ColorFirst: &xlsxTabColor{RGB: "FFD00000"}, - ColorLast: &xlsxTabColor{RGB: "FFD00000"}, - ColorHigh: &xlsxTabColor{RGB: "FFD00000"}, - ColorLow: &xlsxTabColor{RGB: "FFD00000"}, + ColorSeries: &xlsxColor{RGB: "FF323232"}, + ColorNegative: &xlsxColor{RGB: "FFD00000"}, + ColorMarkers: &xlsxColor{RGB: "FFD00000"}, + ColorFirst: &xlsxColor{RGB: "FFD00000"}, + ColorLast: &xlsxColor{RGB: "FFD00000"}, + ColorHigh: &xlsxColor{RGB: "FFD00000"}, + ColorLow: &xlsxColor{RGB: "FFD00000"}, }, // 27 { - ColorSeries: &xlsxTabColor{RGB: "FF000000"}, - ColorNegative: &xlsxTabColor{RGB: "FF0070C0"}, - ColorMarkers: &xlsxTabColor{RGB: "FF0070C0"}, - ColorFirst: &xlsxTabColor{RGB: "FF0070C0"}, - ColorLast: &xlsxTabColor{RGB: "FF0070C0"}, - ColorHigh: &xlsxTabColor{RGB: "FF0070C0"}, - ColorLow: &xlsxTabColor{RGB: "FF0070C0"}, + ColorSeries: &xlsxColor{RGB: "FF000000"}, + ColorNegative: &xlsxColor{RGB: "FF0070C0"}, + ColorMarkers: &xlsxColor{RGB: "FF0070C0"}, + ColorFirst: &xlsxColor{RGB: "FF0070C0"}, + ColorLast: &xlsxColor{RGB: "FF0070C0"}, + ColorHigh: &xlsxColor{RGB: "FF0070C0"}, + ColorLow: &xlsxColor{RGB: "FF0070C0"}, }, // 28 { - ColorSeries: &xlsxTabColor{RGB: "FF376092"}, - ColorNegative: &xlsxTabColor{RGB: "FFD00000"}, - ColorMarkers: &xlsxTabColor{RGB: "FFD00000"}, - ColorFirst: &xlsxTabColor{RGB: "FFD00000"}, - ColorLast: &xlsxTabColor{RGB: "FFD00000"}, - ColorHigh: &xlsxTabColor{RGB: "FFD00000"}, - ColorLow: &xlsxTabColor{RGB: "FFD00000"}, + ColorSeries: &xlsxColor{RGB: "FF376092"}, + ColorNegative: &xlsxColor{RGB: "FFD00000"}, + ColorMarkers: &xlsxColor{RGB: "FFD00000"}, + ColorFirst: &xlsxColor{RGB: "FFD00000"}, + ColorLast: &xlsxColor{RGB: "FFD00000"}, + ColorHigh: &xlsxColor{RGB: "FFD00000"}, + ColorLow: &xlsxColor{RGB: "FFD00000"}, }, // 29 { - ColorSeries: &xlsxTabColor{RGB: "FF0070C0"}, - ColorNegative: &xlsxTabColor{RGB: "FF000000"}, - ColorMarkers: &xlsxTabColor{RGB: "FF000000"}, - ColorFirst: &xlsxTabColor{RGB: "FF000000"}, - ColorLast: &xlsxTabColor{RGB: "FF000000"}, - ColorHigh: &xlsxTabColor{RGB: "FF000000"}, - ColorLow: &xlsxTabColor{RGB: "FF000000"}, + ColorSeries: &xlsxColor{RGB: "FF0070C0"}, + ColorNegative: &xlsxColor{RGB: "FF000000"}, + ColorMarkers: &xlsxColor{RGB: "FF000000"}, + ColorFirst: &xlsxColor{RGB: "FF000000"}, + ColorLast: &xlsxColor{RGB: "FF000000"}, + ColorHigh: &xlsxColor{RGB: "FF000000"}, + ColorLow: &xlsxColor{RGB: "FF000000"}, }, // 30 { - ColorSeries: &xlsxTabColor{RGB: "FF5F5F5F"}, - ColorNegative: &xlsxTabColor{RGB: "FFFFB620"}, - ColorMarkers: &xlsxTabColor{RGB: "FFD70077"}, - ColorFirst: &xlsxTabColor{RGB: "FF5687C2"}, - ColorLast: &xlsxTabColor{RGB: "FF359CEB"}, - ColorHigh: &xlsxTabColor{RGB: "FF56BE79"}, - ColorLow: &xlsxTabColor{RGB: "FFFF5055"}, + ColorSeries: &xlsxColor{RGB: "FF5F5F5F"}, + ColorNegative: &xlsxColor{RGB: "FFFFB620"}, + ColorMarkers: &xlsxColor{RGB: "FFD70077"}, + ColorFirst: &xlsxColor{RGB: "FF5687C2"}, + ColorLast: &xlsxColor{RGB: "FF359CEB"}, + ColorHigh: &xlsxColor{RGB: "FF56BE79"}, + ColorLow: &xlsxColor{RGB: "FFFF5055"}, }, // 31 { - ColorSeries: &xlsxTabColor{RGB: "FF5687C2"}, - ColorNegative: &xlsxTabColor{RGB: "FFFFB620"}, - ColorMarkers: &xlsxTabColor{RGB: "FFD70077"}, - ColorFirst: &xlsxTabColor{RGB: "FF777777"}, - ColorLast: &xlsxTabColor{RGB: "FF359CEB"}, - ColorHigh: &xlsxTabColor{RGB: "FF56BE79"}, - ColorLow: &xlsxTabColor{RGB: "FFFF5055"}, + ColorSeries: &xlsxColor{RGB: "FF5687C2"}, + ColorNegative: &xlsxColor{RGB: "FFFFB620"}, + ColorMarkers: &xlsxColor{RGB: "FFD70077"}, + ColorFirst: &xlsxColor{RGB: "FF777777"}, + ColorLast: &xlsxColor{RGB: "FF359CEB"}, + ColorHigh: &xlsxColor{RGB: "FF56BE79"}, + ColorLow: &xlsxColor{RGB: "FFFF5055"}, }, // 32 { - ColorSeries: &xlsxTabColor{RGB: "FFC6EFCE"}, - ColorNegative: &xlsxTabColor{RGB: "FFFFC7CE"}, - ColorMarkers: &xlsxTabColor{RGB: "FF8CADD6"}, - ColorFirst: &xlsxTabColor{RGB: "FFFFDC47"}, - ColorLast: &xlsxTabColor{RGB: "FFFFEB9C"}, - ColorHigh: &xlsxTabColor{RGB: "FF60D276"}, - ColorLow: &xlsxTabColor{RGB: "FFFF5367"}, + ColorSeries: &xlsxColor{RGB: "FFC6EFCE"}, + ColorNegative: &xlsxColor{RGB: "FFFFC7CE"}, + ColorMarkers: &xlsxColor{RGB: "FF8CADD6"}, + ColorFirst: &xlsxColor{RGB: "FFFFDC47"}, + ColorLast: &xlsxColor{RGB: "FFFFEB9C"}, + ColorHigh: &xlsxColor{RGB: "FF60D276"}, + ColorLow: &xlsxColor{RGB: "FFFF5367"}, }, // 33 { - ColorSeries: &xlsxTabColor{RGB: "FF00B050"}, - ColorNegative: &xlsxTabColor{RGB: "FFFF0000"}, - ColorMarkers: &xlsxTabColor{RGB: "FF0070C0"}, - ColorFirst: &xlsxTabColor{RGB: "FFFFC000"}, - ColorLast: &xlsxTabColor{RGB: "FFFFC000"}, - ColorHigh: &xlsxTabColor{RGB: "FF00B050"}, - ColorLow: &xlsxTabColor{RGB: "FFFF0000"}, + ColorSeries: &xlsxColor{RGB: "FF00B050"}, + ColorNegative: &xlsxColor{RGB: "FFFF0000"}, + ColorMarkers: &xlsxColor{RGB: "FF0070C0"}, + ColorFirst: &xlsxColor{RGB: "FFFFC000"}, + ColorLast: &xlsxColor{RGB: "FFFFC000"}, + ColorHigh: &xlsxColor{RGB: "FF00B050"}, + ColorLow: &xlsxColor{RGB: "FFFF0000"}, }, // 34 { - ColorSeries: &xlsxTabColor{Theme: 3}, - ColorNegative: &xlsxTabColor{Theme: 9}, - ColorMarkers: &xlsxTabColor{Theme: 8}, - ColorFirst: &xlsxTabColor{Theme: 4}, - ColorLast: &xlsxTabColor{Theme: 5}, - ColorHigh: &xlsxTabColor{Theme: 6}, - ColorLow: &xlsxTabColor{Theme: 7}, + ColorSeries: &xlsxColor{Theme: intPtr(3)}, + ColorNegative: &xlsxColor{Theme: intPtr(9)}, + ColorMarkers: &xlsxColor{Theme: intPtr(8)}, + ColorFirst: &xlsxColor{Theme: intPtr(4)}, + ColorLast: &xlsxColor{Theme: intPtr(5)}, + ColorHigh: &xlsxColor{Theme: intPtr(6)}, + ColorLow: &xlsxColor{Theme: intPtr(7)}, }, // 35 { - ColorSeries: &xlsxTabColor{Theme: 1}, - ColorNegative: &xlsxTabColor{Theme: 9}, - ColorMarkers: &xlsxTabColor{Theme: 8}, - ColorFirst: &xlsxTabColor{Theme: 4}, - ColorLast: &xlsxTabColor{Theme: 5}, - ColorHigh: &xlsxTabColor{Theme: 6}, - ColorLow: &xlsxTabColor{Theme: 7}, + ColorSeries: &xlsxColor{Theme: intPtr(1)}, + ColorNegative: &xlsxColor{Theme: intPtr(9)}, + ColorMarkers: &xlsxColor{Theme: intPtr(8)}, + ColorFirst: &xlsxColor{Theme: intPtr(4)}, + ColorLast: &xlsxColor{Theme: intPtr(5)}, + ColorHigh: &xlsxColor{Theme: intPtr(6)}, + ColorLow: &xlsxColor{Theme: intPtr(7)}, }, // 36 } return groups[ID] @@ -427,7 +427,7 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error { group.DisplayXAxis = opts.Axis group.Markers = opts.Markers if opts.SeriesColor != "" { - group.ColorSeries = &xlsxTabColor{ + group.ColorSeries = &xlsxColor{ RGB: getPaletteColor(opts.SeriesColor), } } diff --git a/styles.go b/styles.go index 7c679c26c5..27c48aae7a 100644 --- a/styles.go +++ b/styles.go @@ -753,7 +753,7 @@ var currencyNumFmt = map[int]string{ // builtInNumFmtFunc defined the format conversion functions map. Partial format // code doesn't support currently and will return original string. -var builtInNumFmtFunc = map[int]func(v, format string, date1904 bool) string{ +var builtInNumFmtFunc = map[int]func(v, format string, date1904 bool, cellType CellType) string{ 0: format, 1: formatToInt, 2: formatToFloat, @@ -892,8 +892,8 @@ func printCommaSep(text string) string { // formatToInt provides a function to convert original string to integer // format as string type by given built-in number formats code and cell // string. -func formatToInt(v, format string, date1904 bool) string { - if strings.Contains(v, "_") { +func formatToInt(v, format string, date1904 bool, cellType CellType) string { + if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { return v } f, err := strconv.ParseFloat(v, 64) @@ -906,8 +906,8 @@ func formatToInt(v, format string, date1904 bool) string { // formatToFloat provides a function to convert original string to float // format as string type by given built-in number formats code and cell // string. -func formatToFloat(v, format string, date1904 bool) string { - if strings.Contains(v, "_") { +func formatToFloat(v, format string, date1904 bool, cellType CellType) string { + if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { return v } f, err := strconv.ParseFloat(v, 64) @@ -924,8 +924,8 @@ func formatToFloat(v, format string, date1904 bool) string { // formatToIntSeparator provides a function to convert original string to // integer format as string type by given built-in number formats code and cell // string. -func formatToIntSeparator(v, format string, date1904 bool) string { - if strings.Contains(v, "_") { +func formatToIntSeparator(v, format string, date1904 bool, cellType CellType) string { + if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { return v } f, err := strconv.ParseFloat(v, 64) @@ -937,8 +937,8 @@ func formatToIntSeparator(v, format string, date1904 bool) string { // formatToA provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. -func formatToA(v, format string, date1904 bool) string { - if strings.Contains(v, "_") { +func formatToA(v, format string, date1904 bool, cellType CellType) string { + if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { return v } f, err := strconv.ParseFloat(v, 64) @@ -960,8 +960,8 @@ func formatToA(v, format string, date1904 bool) string { // formatToB provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. -func formatToB(v, format string, date1904 bool) string { - if strings.Contains(v, "_") { +func formatToB(v, format string, date1904 bool, cellType CellType) string { + if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { return v } f, err := strconv.ParseFloat(v, 64) @@ -990,8 +990,8 @@ func formatToB(v, format string, date1904 bool) string { // formatToC provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. -func formatToC(v, format string, date1904 bool) string { - if strings.Contains(v, "_") { +func formatToC(v, format string, date1904 bool, cellType CellType) string { + if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { return v } f, err := strconv.ParseFloat(v, 64) @@ -1007,8 +1007,8 @@ func formatToC(v, format string, date1904 bool) string { // formatToD provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. -func formatToD(v, format string, date1904 bool) string { - if strings.Contains(v, "_") { +func formatToD(v, format string, date1904 bool, cellType CellType) string { + if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { return v } f, err := strconv.ParseFloat(v, 64) @@ -1024,8 +1024,8 @@ func formatToD(v, format string, date1904 bool) string { // formatToE provides a function to convert original string to special format // as string type by given built-in number formats code and cell string. -func formatToE(v, format string, date1904 bool) string { - if strings.Contains(v, "_") { +func formatToE(v, format string, date1904 bool, cellType CellType) string { + if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { return v } f, err := strconv.ParseFloat(v, 64) diff --git a/xmlChartSheet.go b/xmlChartSheet.go index 16599fd25c..a710871d44 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -35,10 +35,10 @@ type xlsxChartsheet struct { // xlsxChartsheetPr specifies chart sheet properties. type xlsxChartsheetPr struct { - XMLName xml.Name `xml:"sheetPr"` - PublishedAttr bool `xml:"published,attr,omitempty"` - CodeNameAttr string `xml:"codeName,attr,omitempty"` - TabColor *xlsxTabColor `xml:"tabColor"` + XMLName xml.Name `xml:"sheetPr"` + PublishedAttr bool `xml:"published,attr,omitempty"` + CodeNameAttr string `xml:"codeName,attr,omitempty"` + TabColor *xlsxColor `xml:"tabColor"` } // xlsxChartsheetViews specifies chart sheet views. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 97bbfdd4a0..8e89761860 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -241,7 +241,7 @@ type xlsxSheetPr struct { CodeName string `xml:"codeName,attr,omitempty"` FilterMode bool `xml:"filterMode,attr,omitempty"` EnableFormatConditionsCalculation *bool `xml:"enableFormatConditionsCalculation,attr"` - TabColor *xlsxTabColor `xml:"tabColor"` + TabColor *xlsxColor `xml:"tabColor"` OutlinePr *xlsxOutlinePr `xml:"outlinePr"` PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr"` } @@ -261,15 +261,6 @@ type xlsxPageSetUpPr struct { FitToPage bool `xml:"fitToPage,attr,omitempty"` } -// xlsxTabColor represents background color of the sheet tab. -type xlsxTabColor struct { - Auto bool `xml:"auto,attr,omitempty"` - Indexed int `xml:"indexed,attr,omitempty"` - RGB string `xml:"rgb,attr,omitempty"` - Theme int `xml:"theme,attr,omitempty"` - Tint float64 `xml:"tint,attr,omitempty"` -} - // xlsxCols defines column width and column formatting for one or more columns // of the worksheet. type xlsxCols struct { @@ -850,14 +841,14 @@ type xlsxX14SparklineGroup struct { MinAxisType string `xml:"minAxisType,attr,omitempty"` MaxAxisType string `xml:"maxAxisType,attr,omitempty"` RightToLeft bool `xml:"rightToLeft,attr,omitempty"` - ColorSeries *xlsxTabColor `xml:"x14:colorSeries"` - ColorNegative *xlsxTabColor `xml:"x14:colorNegative"` + ColorSeries *xlsxColor `xml:"x14:colorSeries"` + ColorNegative *xlsxColor `xml:"x14:colorNegative"` ColorAxis *xlsxColor `xml:"x14:colorAxis"` - ColorMarkers *xlsxTabColor `xml:"x14:colorMarkers"` - ColorFirst *xlsxTabColor `xml:"x14:colorFirst"` - ColorLast *xlsxTabColor `xml:"x14:colorLast"` - ColorHigh *xlsxTabColor `xml:"x14:colorHigh"` - ColorLow *xlsxTabColor `xml:"x14:colorLow"` + ColorMarkers *xlsxColor `xml:"x14:colorMarkers"` + ColorFirst *xlsxColor `xml:"x14:colorFirst"` + ColorLast *xlsxColor `xml:"x14:colorLast"` + ColorHigh *xlsxColor `xml:"x14:colorHigh"` + ColorLow *xlsxColor `xml:"x14:colorLow"` Sparklines xlsxX14Sparklines `xml:"x14:sparklines"` } From 63d8a09082e627654c7452af7d126e0a503c8ec9 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 21 Apr 2023 08:51:04 +0800 Subject: [PATCH 185/213] Breaking changes: rename exported variable `ErrTableNameLength` to `ErrNameLength` - Check the defined name - Improve the cell comment box shape size compatibility with KingSoft WPS - Update unit test --- errors.go | 14 +++++++------- sheet.go | 3 +++ stream_test.go | 2 +- table.go | 12 ++++++------ table_test.go | 21 ++++++++++++--------- vmlDrawing.go | 4 ++-- 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/errors.go b/errors.go index 7c7143c65c..1a6cc8a07e 100644 --- a/errors.go +++ b/errors.go @@ -40,10 +40,10 @@ func newInvalidExcelDateError(dateValue float64) error { return fmt.Errorf("invalid date value %f, negative values are not supported", dateValue) } -// newInvalidTableNameError defined the error message on receiving the invalid -// table name. -func newInvalidTableNameError(name string) error { - return fmt.Errorf("invalid table name %q", name) +// newInvalidNameError defined the error message on receiving the invalid +// defined name or table name. +func newInvalidNameError(name string) error { + return fmt.Errorf("invalid name %q, the name should be starts with a letter or underscore, can not include a space or character, and can not conflict with an existing name in the workbook", name) } // newUnsupportedChartType defined the error message on receiving the chart @@ -236,9 +236,9 @@ var ( // ErrSheetNameLength defined the error message on receiving the sheet // name length exceeds the limit. ErrSheetNameLength = fmt.Errorf("the sheet name length exceeds the %d characters limit", MaxSheetNameLength) - // ErrTableNameLength defined the error message on receiving the table name - // length exceeds the limit. - ErrTableNameLength = fmt.Errorf("the table name length exceeds the %d characters limit", MaxFieldLength) + // ErrNameLength defined the error message on receiving the defined name or + // table name length exceeds the limit. + ErrNameLength = fmt.Errorf("the name length exceeds the %d characters limit", MaxFieldLength) // ErrExistsTableName defined the error message on given table already exists. ErrExistsTableName = errors.New("the same name table already exists") // ErrCellStyles defined the error message on cell styles exceeds the limit. diff --git a/sheet.go b/sheet.go index a6e3d032b8..92aa14656a 100644 --- a/sheet.go +++ b/sheet.go @@ -1551,6 +1551,9 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { if definedName.Name == "" || definedName.RefersTo == "" { return ErrParameterInvalid } + if err := checkDefinedName(definedName.Name); err != nil { + return err + } wb, err := f.workbookReader() if err != nil { return err diff --git a/stream_test.go b/stream_test.go index 720f59854d..d5f3ed21fd 100644 --- a/stream_test.go +++ b/stream_test.go @@ -223,7 +223,7 @@ func TestStreamTable(t *testing.T) { assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A1:B"}), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) // Test add table with invalid table name - assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"}), newInvalidTableNameError("1Table").Error()) + assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"}), newInvalidNameError("1Table").Error()) // Test add table with unsupported charset content types file.ContentTypes = nil file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) diff --git a/table.go b/table.go index 386942724a..9cadd8c0a8 100644 --- a/table.go +++ b/table.go @@ -33,7 +33,7 @@ func parseTableOptions(opts *Table) (*Table, error) { if opts.ShowRowStripes == nil { opts.ShowRowStripes = boolPtr(true) } - if err = checkTableName(opts.Name); err != nil { + if err = checkDefinedName(opts.Name); err != nil { return opts, err } return opts, err @@ -182,13 +182,13 @@ func (f *File) setTableHeader(sheet string, showHeaderRow bool, x1, y1, x2 int) return tableColumns, nil } -// checkSheetName check whether there are illegal characters in the table name. -// Verify that the name: +// checkDefinedName check whether there are illegal characters in the defined +// name or table name. Verify that the name: // 1. Starts with a letter or underscore (_) // 2. Doesn't include a space or character that isn't allowed -func checkTableName(name string) error { +func checkDefinedName(name string) error { if utf8.RuneCountInString(name) > MaxFieldLength { - return ErrTableNameLength + return ErrNameLength } for i, c := range name { if string(c) == "_" { @@ -200,7 +200,7 @@ func checkTableName(name string) error { if i > 0 && unicode.IsDigit(c) { continue } - return newInvalidTableNameError(name) + return newInvalidNameError(name) } return nil } diff --git a/table_test.go b/table_test.go index 6eec13980d..e6a67fb9a9 100644 --- a/table_test.go +++ b/table_test.go @@ -46,24 +46,27 @@ func TestAddTable(t *testing.T) { f = NewFile() assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell reference [0, 0]") assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell reference [0, 0]") - // Test add table with invalid table name + // Test set defined name and add table with invalid name for _, cases := range []struct { name string err error }{ - {name: "1Table", err: newInvalidTableNameError("1Table")}, - {name: "-Table", err: newInvalidTableNameError("-Table")}, - {name: "'Table", err: newInvalidTableNameError("'Table")}, - {name: "Table 1", err: newInvalidTableNameError("Table 1")}, - {name: "A&B", err: newInvalidTableNameError("A&B")}, - {name: "_1Table'", err: newInvalidTableNameError("_1Table'")}, - {name: "\u0f5f\u0fb3\u0f0b\u0f21", err: newInvalidTableNameError("\u0f5f\u0fb3\u0f0b\u0f21")}, - {name: strings.Repeat("c", MaxFieldLength+1), err: ErrTableNameLength}, + {name: "1Table", err: newInvalidNameError("1Table")}, + {name: "-Table", err: newInvalidNameError("-Table")}, + {name: "'Table", err: newInvalidNameError("'Table")}, + {name: "Table 1", err: newInvalidNameError("Table 1")}, + {name: "A&B", err: newInvalidNameError("A&B")}, + {name: "_1Table'", err: newInvalidNameError("_1Table'")}, + {name: "\u0f5f\u0fb3\u0f0b\u0f21", err: newInvalidNameError("\u0f5f\u0fb3\u0f0b\u0f21")}, + {name: strings.Repeat("c", MaxFieldLength+1), err: ErrNameLength}, } { assert.EqualError(t, f.AddTable("Sheet1", &Table{ Range: "A1:B2", Name: cases.name, }), cases.err.Error()) + assert.EqualError(t, f.SetDefinedName(&DefinedName{ + Name: cases.name, RefersTo: "Sheet1!$A$2:$D$5", + }), cases.err.Error()) } // Test check duplicate table name with unsupported charset table parts f = NewFile() diff --git a/vmlDrawing.go b/vmlDrawing.go index be1212e649..1a49d72212 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -117,8 +117,8 @@ type xlsxDiv struct { // element. type xClientData struct { ObjectType string `xml:"ObjectType,attr"` - MoveWithCells string `xml:"x:MoveWithCells,omitempty"` - SizeWithCells string `xml:"x:SizeWithCells,omitempty"` + MoveWithCells string `xml:"x:MoveWithCells"` + SizeWithCells string `xml:"x:SizeWithCells"` Anchor string `xml:"x:Anchor"` AutoFill string `xml:"x:AutoFill"` Row int `xml:"x:Row"` From 787453c6f0b256fdf2c0454a0a694ee79ea52675 Mon Sep 17 00:00:00 2001 From: Chen Zhidong Date: Sun, 23 Apr 2023 18:00:31 +0800 Subject: [PATCH 186/213] Optimizing regexp calls to improve performance (#1532) --- calc.go | 50 +++++++++++++++++++++++--------------------------- sheet.go | 2 +- table.go | 17 +++++++++++------ 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/calc.go b/calc.go index faea6d743d..4d56fce65e 100644 --- a/calc.go +++ b/calc.go @@ -193,6 +193,24 @@ var ( return fmt.Sprintf("R[%d]C[%d]", row, col), nil }, } + formularFormats = []*regexp.Regexp{ + regexp.MustCompile(`^(\d+)$`), + regexp.MustCompile(`^=(.*)$`), + regexp.MustCompile(`^<>(.*)$`), + regexp.MustCompile(`^<=(.*)$`), + regexp.MustCompile(`^>=(.*)$`), + regexp.MustCompile(`^<(.*)$`), + regexp.MustCompile(`^>(.*)$`), + } + formularCriterias = []byte{ + criteriaEq, + criteriaEq, + criteriaNe, + criteriaLe, + criteriaGe, + criteriaL, + criteriaG, + } ) // calcContext defines the formula execution context. @@ -1654,33 +1672,11 @@ func formulaCriteriaParser(exp string) (fc *formulaCriteria) { if exp == "" { return } - if match := regexp.MustCompile(`^(\d+)$`).FindStringSubmatch(exp); len(match) > 1 { - fc.Type, fc.Condition = criteriaEq, match[1] - return - } - if match := regexp.MustCompile(`^=(.*)$`).FindStringSubmatch(exp); len(match) > 1 { - fc.Type, fc.Condition = criteriaEq, match[1] - return - } - if match := regexp.MustCompile(`^<>(.*)$`).FindStringSubmatch(exp); len(match) > 1 { - fc.Type, fc.Condition = criteriaNe, match[1] - return - } - if match := regexp.MustCompile(`^<=(.*)$`).FindStringSubmatch(exp); len(match) > 1 { - fc.Type, fc.Condition = criteriaLe, match[1] - return - } - if match := regexp.MustCompile(`^>=(.*)$`).FindStringSubmatch(exp); len(match) > 1 { - fc.Type, fc.Condition = criteriaGe, match[1] - return - } - if match := regexp.MustCompile(`^<(.*)$`).FindStringSubmatch(exp); len(match) > 1 { - fc.Type, fc.Condition = criteriaL, match[1] - return - } - if match := regexp.MustCompile(`^>(.*)$`).FindStringSubmatch(exp); len(match) > 1 { - fc.Type, fc.Condition = criteriaG, match[1] - return + for i, re := range formularFormats { + if match := re.FindStringSubmatch(exp); len(match) > 1 { + fc.Type, fc.Condition = formularCriterias[i], match[1] + return + } } if strings.Contains(exp, "?") { exp = strings.ReplaceAll(exp, "?", ".") diff --git a/sheet.go b/sheet.go index 92aa14656a..a022eb0904 100644 --- a/sheet.go +++ b/sheet.go @@ -977,6 +977,7 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, if sst, err = f.sharedStringsReader(); err != nil { return } + regex := regexp.MustCompile(value) decoder := f.xmlNewDecoder(bytes.NewReader(f.readBytes(name))) for { var token xml.Token @@ -1001,7 +1002,6 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, _ = decoder.DecodeElement(&colCell, &xmlElement) val, _ := colCell.getValueFrom(f, sst, false) if regSearch { - regex := regexp.MustCompile(value) if !regex.MatchString(val) { continue } diff --git a/table.go b/table.go index 9cadd8c0a8..8b375a8a3f 100644 --- a/table.go +++ b/table.go @@ -23,6 +23,13 @@ import ( "unicode/utf8" ) +var ( + expressionFormat = regexp.MustCompile(`"(?:[^"]|"")*"|\S+`) + conditionFormat = regexp.MustCompile(`(or|\|\|)`) + blankFormat = regexp.MustCompile("blanks|nonblanks") + matchFormat = regexp.MustCompile("[*?]") +) + // parseTableOptions provides a function to parse the format settings of the // table with default value. func parseTableOptions(opts *Table) (*Table, error) { @@ -400,8 +407,7 @@ func (f *File) autoFilter(sheet, ref string, columns, col int, opts []AutoFilter return fmt.Errorf("incorrect index of column '%s'", opt.Column) } fc := &xlsxFilterColumn{ColID: offset} - re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`) - token := re.FindAllString(opt.Expression, -1) + token := expressionFormat.FindAllString(opt.Expression, -1) if len(token) != 3 && len(token) != 7 { return fmt.Errorf("incorrect number of tokens in criteria '%s'", opt.Expression) } @@ -484,8 +490,7 @@ func (f *File) parseFilterExpression(expression string, tokens []string) ([]int, // expressions). conditional := 0 c := tokens[3] - re, _ := regexp.Match(`(or|\|\|)`, []byte(c)) - if re { + if conditionFormat.Match([]byte(c)) { conditional = 1 } expression1, token1, err := f.parseFilterTokens(expression, tokens[:3]) @@ -533,7 +538,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str } token := tokens[2] // Special handling for Blanks/NonBlanks. - re, _ := regexp.Match("blanks|nonblanks", []byte(strings.ToLower(token))) + re := blankFormat.Match([]byte(strings.ToLower(token))) if re { // Only allow Equals or NotEqual in this context. if operator != 2 && operator != 5 { @@ -558,7 +563,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str } // If the string token contains an Excel match character then change the // operator type to indicate a non "simple" equality. - re, _ = regexp.Match("[*?]", []byte(token)) + re = matchFormat.Match([]byte(token)) if operator == 2 && re { operator = 22 } From 93c72b4d55919cf476a7c27d2ee8f62c482fa507 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 24 Apr 2023 00:02:13 +0800 Subject: [PATCH 187/213] This optimizes internal functions signature and mutex declarations --- adjust.go | 4 +- calc.go | 10 ++--- calcchain.go | 4 +- cell.go | 92 +++++++++++++++++++++++----------------------- col.go | 44 +++++++++++----------- comment.go | 4 +- drawing.go | 4 +- excelize.go | 32 ++++++++-------- merge.go | 8 ++-- merge_test.go | 4 +- picture.go | 36 +++++++++--------- rows.go | 32 ++++++++-------- sheet.go | 26 ++++++------- styles.go | 22 +++++------ workbook.go | 4 +- xmlContentTypes.go | 2 +- xmlDrawing.go | 2 +- xmlStyles.go | 2 +- xmlWorkbook.go | 2 +- xmlWorksheet.go | 2 +- 20 files changed, 168 insertions(+), 168 deletions(-) diff --git a/adjust.go b/adjust.go index b6e16e74aa..216f4c8c5b 100644 --- a/adjust.go +++ b/adjust.go @@ -60,8 +60,8 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) if err = f.adjustCalcChain(dir, num, offset, sheetID); err != nil { return err } - checkSheet(ws) - _ = checkRow(ws) + ws.checkSheet() + _ = ws.checkRow() if ws.MergeCells != nil && len(ws.MergeCells.Cells) == 0 { ws.MergeCells = nil diff --git a/calc.go b/calc.go index 4d56fce65e..96abd64c6e 100644 --- a/calc.go +++ b/calc.go @@ -215,7 +215,7 @@ var ( // calcContext defines the formula execution context. type calcContext struct { - sync.Mutex + mu sync.Mutex entry string maxCalcIterations uint iterations map[string]uint @@ -1553,19 +1553,19 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e ) ref := fmt.Sprintf("%s!%s", sheet, cell) if formula, _ := f.GetCellFormula(sheet, cell); len(formula) != 0 { - ctx.Lock() + ctx.mu.Lock() if ctx.entry != ref { if ctx.iterations[ref] <= f.options.MaxCalcIterations { ctx.iterations[ref]++ - ctx.Unlock() + ctx.mu.Unlock() arg, _ = f.calcCellValue(ctx, sheet, cell) ctx.iterationsCache[ref] = arg return arg, nil } - ctx.Unlock() + ctx.mu.Unlock() return ctx.iterationsCache[ref], nil } - ctx.Unlock() + ctx.mu.Unlock() } if value, err = f.GetCellValue(sheet, cell, Options{RawCellValue: true}); err != nil { return arg, err diff --git a/calcchain.go b/calcchain.go index 915508e7f4..c35dd7d480 100644 --- a/calcchain.go +++ b/calcchain.go @@ -58,8 +58,8 @@ func (f *File) deleteCalcChain(index int, cell string) error { if err != nil { return err } - content.Lock() - defer content.Unlock() + content.mu.Lock() + defer content.mu.Unlock() for k, v := range content.Overrides { if v.PartName == "/xl/calcChain.xml" { content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...) diff --git a/cell.go b/cell.go index 52d8186046..229267fc8c 100644 --- a/cell.go +++ b/cell.go @@ -236,13 +236,13 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error { if err != nil { return err } - c, col, row, err := f.prepareCell(ws, cell) + c, col, row, err := ws.prepareCell(cell) if err != nil { return err } - ws.Lock() - c.S = f.prepareCellStyle(ws, col, row, c.S) - ws.Unlock() + ws.mu.Lock() + c.S = ws.prepareCellStyle(col, row, c.S) + ws.mu.Unlock() var date1904, isNum bool wb, err := f.workbookReader() if err != nil { @@ -292,13 +292,13 @@ func (f *File) SetCellInt(sheet, cell string, value int) error { if err != nil { return err } - c, col, row, err := f.prepareCell(ws, cell) + c, col, row, err := ws.prepareCell(cell) if err != nil { return err } - ws.Lock() - defer ws.Unlock() - c.S = f.prepareCellStyle(ws, col, row, c.S) + ws.mu.Lock() + defer ws.mu.Unlock() + c.S = ws.prepareCellStyle(col, row, c.S) c.T, c.V = setCellInt(value) c.IS = nil return f.removeFormula(c, ws, sheet) @@ -318,13 +318,13 @@ func (f *File) SetCellBool(sheet, cell string, value bool) error { if err != nil { return err } - c, col, row, err := f.prepareCell(ws, cell) + c, col, row, err := ws.prepareCell(cell) if err != nil { return err } - ws.Lock() - defer ws.Unlock() - c.S = f.prepareCellStyle(ws, col, row, c.S) + ws.mu.Lock() + defer ws.mu.Unlock() + c.S = ws.prepareCellStyle(col, row, c.S) c.T, c.V = setCellBool(value) c.IS = nil return f.removeFormula(c, ws, sheet) @@ -355,13 +355,13 @@ func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSiz if err != nil { return err } - c, col, row, err := f.prepareCell(ws, cell) + c, col, row, err := ws.prepareCell(cell) if err != nil { return err } - ws.Lock() - defer ws.Unlock() - c.S = f.prepareCellStyle(ws, col, row, c.S) + ws.mu.Lock() + defer ws.mu.Unlock() + c.S = ws.prepareCellStyle(col, row, c.S) c.T, c.V = setCellFloat(value, precision, bitSize) c.IS = nil return f.removeFormula(c, ws, sheet) @@ -381,13 +381,13 @@ func (f *File) SetCellStr(sheet, cell, value string) error { if err != nil { return err } - c, col, row, err := f.prepareCell(ws, cell) + c, col, row, err := ws.prepareCell(cell) if err != nil { return err } - ws.Lock() - defer ws.Unlock() - c.S = f.prepareCellStyle(ws, col, row, c.S) + ws.mu.Lock() + defer ws.mu.Unlock() + c.S = ws.prepareCellStyle(col, row, c.S) if c.T, c.V, err = f.setCellString(value); err != nil { return err } @@ -413,8 +413,8 @@ func (f *File) setCellString(value string) (t, v string, err error) { // sharedStringsLoader load shared string table from system temporary file to // memory, and reset shared string table for reader. func (f *File) sharedStringsLoader() (err error) { - f.Lock() - defer f.Unlock() + f.mu.Lock() + defer f.mu.Unlock() if path, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { f.Pkg.Store(defaultXMLPathSharedStrings, f.readBytes(defaultXMLPathSharedStrings)) f.tempFiles.Delete(defaultXMLPathSharedStrings) @@ -443,8 +443,8 @@ func (f *File) setSharedString(val string) (int, error) { if err != nil { return 0, err } - f.Lock() - defer f.Unlock() + f.mu.Lock() + defer f.mu.Unlock() if i, ok := f.sharedStringsMap[val]; ok { return i, nil } @@ -558,8 +558,8 @@ func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) { // intended to be used with for range on rows an argument with the spreadsheet // opened file. func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { - f.Lock() - defer f.Unlock() + f.mu.Lock() + defer f.mu.Unlock() switch c.T { case "b": return c.getCellBool(f, raw) @@ -600,13 +600,13 @@ func (f *File) SetCellDefault(sheet, cell, value string) error { if err != nil { return err } - c, col, row, err := f.prepareCell(ws, cell) + c, col, row, err := ws.prepareCell(cell) if err != nil { return err } - ws.Lock() - defer ws.Unlock() - c.S = f.prepareCellStyle(ws, col, row, c.S) + ws.mu.Lock() + defer ws.mu.Unlock() + c.S = ws.prepareCellStyle(col, row, c.S) c.setCellDefault(value) return f.removeFormula(c, ws, sheet) } @@ -718,7 +718,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) if err != nil { return err } - c, _, _, err := f.prepareCell(ws, cell) + c, _, _, err := ws.prepareCell(cell) if err != nil { return err } @@ -763,7 +763,7 @@ func (ws *xlsxWorksheet) setSharedFormula(ref string) error { cnt := ws.countSharedFormula() for c := coordinates[0]; c <= coordinates[2]; c++ { for r := coordinates[1]; r <= coordinates[3]; r++ { - prepareSheetXML(ws, c, r) + ws.prepareSheetXML(c, r) cell := &ws.SheetData.Row[r-1].C[c-1] if cell.F == nil { cell.F = &xlsxF{} @@ -867,7 +867,7 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype if err != nil { return err } - if cell, err = f.mergeCellsParser(ws, cell); err != nil { + if cell, err = ws.mergeCellsParser(cell); err != nil { return err } @@ -944,7 +944,7 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro if err != nil { return } - c, _, _, err := f.prepareCell(ws, cell) + c, _, _, err := ws.prepareCell(cell) if err != nil { return } @@ -1171,14 +1171,14 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { if err != nil { return err } - c, col, row, err := f.prepareCell(ws, cell) + c, col, row, err := ws.prepareCell(cell) if err != nil { return err } if err := f.sharedStringsLoader(); err != nil { return err } - c.S = f.prepareCellStyle(ws, col, row, c.S) + c.S = ws.prepareCellStyle(col, row, c.S) si := xlsxSI{} sst, err := f.sharedStringsReader() if err != nil { @@ -1252,9 +1252,9 @@ func (f *File) setSheetCells(sheet, cell string, slice interface{}, dir adjustDi } // getCellInfo does common preparation for all set cell value functions. -func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, error) { +func (ws *xlsxWorksheet) prepareCell(cell string) (*xlsxC, int, int, error) { var err error - cell, err = f.mergeCellsParser(ws, cell) + cell, err = ws.mergeCellsParser(cell) if err != nil { return nil, 0, 0, err } @@ -1263,9 +1263,9 @@ func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, er return nil, 0, 0, err } - prepareSheetXML(ws, col, row) - ws.Lock() - defer ws.Unlock() + ws.prepareSheetXML(col, row) + ws.mu.Lock() + defer ws.mu.Unlock() return &ws.SheetData.Row[row-1].C[col-1], col, row, err } @@ -1277,7 +1277,7 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c if err != nil { return "", err } - cell, err = f.mergeCellsParser(ws, cell) + cell, err = ws.mergeCellsParser(cell) if err != nil { return "", err } @@ -1286,8 +1286,8 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c return "", err } - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() lastRowNum := 0 if l := len(ws.SheetData.Row); l > 0 { @@ -1366,7 +1366,7 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er // prepareCellStyle provides a function to prepare style index of cell in // worksheet by given column index and style index. -func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int { +func (ws *xlsxWorksheet) prepareCellStyle(col, row, style int) int { if style != 0 { return style } @@ -1387,7 +1387,7 @@ func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int { // mergeCellsParser provides a function to check merged cells in worksheet by // given cell reference. -func (f *File) mergeCellsParser(ws *xlsxWorksheet, cell string) (string, error) { +func (ws *xlsxWorksheet) mergeCellsParser(cell string) (string, error) { cell = strings.ToUpper(cell) col, row, err := CellNameToCoordinates(cell) if err != nil { diff --git a/col.go b/col.go index 74f9cddaaf..dd50fb4793 100644 --- a/col.go +++ b/col.go @@ -215,11 +215,11 @@ func (f *File) Cols(sheet string) (*Cols, error) { if !ok { return nil, ErrSheetNotExist{sheet} } - if ws, ok := f.Sheet.Load(name); ok && ws != nil { - worksheet := ws.(*xlsxWorksheet) - worksheet.Lock() - defer worksheet.Unlock() - output, _ := xml.Marshal(worksheet) + if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil { + ws := worksheet.(*xlsxWorksheet) + ws.mu.Lock() + defer ws.mu.Unlock() + output, _ := xml.Marshal(ws) f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } var colIterator columnXMLIterator @@ -261,8 +261,8 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { if err != nil { return false, err } - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() if ws.Cols == nil { return true, err } @@ -295,8 +295,8 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error { if err != nil { return err } - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() colData := xlsxCol{ Min: min, Max: max, @@ -432,17 +432,17 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { if err != nil { return err } - s.Lock() + s.mu.Lock() if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { - s.Unlock() + s.mu.Unlock() return newInvalidStyleID(styleID) } - s.Unlock() + s.mu.Unlock() ws, err := f.workSheetReader(sheet) if err != nil { return err } - ws.Lock() + ws.mu.Lock() if ws.Cols == nil { ws.Cols = &xlsxCols{} } @@ -461,7 +461,7 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { fc.Width = c.Width return fc }) - ws.Unlock() + ws.mu.Unlock() if rows := len(ws.SheetData.Row); rows > 0 { for col := min; col <= max; col++ { from, _ := CoordinatesToCellName(col, 1) @@ -488,8 +488,8 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error if err != nil { return err } - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() col := xlsxCol{ Min: min, Max: max, @@ -634,8 +634,8 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh // sheet name and column number. func (f *File) getColWidth(sheet string, col int) int { ws, _ := f.workSheetReader(sheet) - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() if ws.Cols != nil { var width float64 for _, v := range ws.Cols.Col { @@ -663,8 +663,8 @@ func (f *File) GetColStyle(sheet, col string) (int, error) { if err != nil { return styleID, err } - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() if ws.Cols != nil { for _, v := range ws.Cols.Col { if v.Min <= colNum && colNum <= v.Max { @@ -686,8 +686,8 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { if err != nil { return defaultColWidth, err } - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() if ws.Cols != nil { var width float64 for _, v := range ws.Cols.Col { diff --git a/comment.go b/comment.go index 25564cbded..28ba40bce5 100644 --- a/comment.go +++ b/comment.go @@ -68,8 +68,8 @@ func (f *File) GetComments(sheet string) ([]Comment, error) { func (f *File) getSheetComments(sheetFile string) string { rels, _ := f.relsReader("xl/worksheets/_rels/" + sheetFile + ".rels") if sheetRels := rels; sheetRels != nil { - sheetRels.Lock() - defer sheetRels.Unlock() + sheetRels.mu.Lock() + defer sheetRels.mu.Unlock() for _, v := range sheetRels.Relationships { if v.Type == SourceRelationshipComments { return v.Target diff --git a/drawing.go b/drawing.go index f04dc336ab..41302e429d 100644 --- a/drawing.go +++ b/drawing.go @@ -1285,8 +1285,8 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int, error) { if drawing, ok := f.Drawings.Load(path); ok && drawing != nil { wsDr = drawing.(*xlsxWsDr) } - wsDr.Lock() - defer wsDr.Unlock() + wsDr.mu.Lock() + defer wsDr.mu.Unlock() return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2, nil } diff --git a/excelize.go b/excelize.go index 9903fbff5b..2c75f1f0ce 100644 --- a/excelize.go +++ b/excelize.go @@ -28,7 +28,7 @@ import ( // File define a populated spreadsheet file struct. type File struct { - sync.Mutex + mu sync.Mutex options *Options xmlAttr map[string][]xml.Attr checked map[string]bool @@ -234,8 +234,8 @@ func (f *File) setDefaultTimeStyle(sheet, cell string, format int) error { // workSheetReader provides a function to get the pointer to the structure // after deserialization by given worksheet name. func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { - f.Lock() - defer f.Unlock() + f.mu.Lock() + defer f.mu.Unlock() var ( name string ok bool @@ -271,8 +271,8 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { f.checked = make(map[string]bool) } if ok = f.checked[name]; !ok { - checkSheet(ws) - if err = checkRow(ws); err != nil { + ws.checkSheet() + if err = ws.checkRow(); err != nil { return } f.checked[name] = true @@ -283,7 +283,7 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { // checkSheet provides a function to fill each row element and make that is // continuous in a worksheet of XML. -func checkSheet(ws *xlsxWorksheet) { +func (ws *xlsxWorksheet) checkSheet() { var row int var r0 xlsxRow for i, r := range ws.SheetData.Row { @@ -319,13 +319,13 @@ func checkSheet(ws *xlsxWorksheet) { for i := 1; i <= row; i++ { sheetData.Row[i-1].R = i } - checkSheetR0(ws, &sheetData, &r0) + ws.checkSheetR0(&sheetData, &r0) } // checkSheetR0 handle the row element with r="0" attribute, cells in this row // could be disorderly, the cell in this row can be used as the value of // which cell is empty in the normal rows. -func checkSheetR0(ws *xlsxWorksheet, sheetData *xlsxSheetData, r0 *xlsxRow) { +func (ws *xlsxWorksheet) checkSheetR0(sheetData *xlsxSheetData, r0 *xlsxRow) { for _, cell := range r0.C { if col, row, err := CellNameToCoordinates(cell.R); err == nil { rows, rowIdx := len(sheetData.Row), row-1 @@ -351,8 +351,8 @@ func (f *File) setRels(rID, relPath, relType, target, targetMode string) int { if rels == nil || rID == "" { return f.addRels(relPath, relType, target, targetMode) } - rels.Lock() - defer rels.Unlock() + rels.mu.Lock() + defer rels.mu.Unlock() var ID int for i, rel := range rels.Relationships { if rel.ID == rID { @@ -376,8 +376,8 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { if rels == nil { rels = &xlsxRelationships{} } - rels.Lock() - defer rels.Unlock() + rels.mu.Lock() + defer rels.mu.Unlock() var rID int for idx, rel := range rels.Relationships { ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId")) @@ -490,8 +490,8 @@ func (f *File) AddVBAProject(file []byte) error { if err != nil { return err } - rels.Lock() - defer rels.Unlock() + rels.mu.Lock() + defer rels.mu.Unlock() var rID int var ok bool for _, rel := range rels.Relationships { @@ -524,8 +524,8 @@ func (f *File) setContentTypePartProjectExtensions(contentType string) error { if err != nil { return err } - content.Lock() - defer content.Unlock() + content.mu.Lock() + defer content.mu.Unlock() for _, v := range content.Defaults { if v.Extension == "bin" { ok = true diff --git a/merge.go b/merge.go index eb3fea30ca..af4e6420c0 100644 --- a/merge.go +++ b/merge.go @@ -64,8 +64,8 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error { if err != nil { return err } - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() ref := hCell + ":" + vCell if ws.MergeCells != nil { ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect}) @@ -87,8 +87,8 @@ func (f *File) UnmergeCell(sheet, hCell, vCell string) error { if err != nil { return err } - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() rect1, err := rangeRefToCoordinates(hCell + ":" + vCell) if err != nil { return err diff --git a/merge_test.go b/merge_test.go index 6c9d2025a9..2f15a3d5bb 100644 --- a/merge_test.go +++ b/merge_test.go @@ -207,7 +207,7 @@ func TestFlatMergedCells(t *testing.T) { } func TestMergeCellsParser(t *testing.T) { - f := NewFile() - _, err := f.mergeCellsParser(&xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{nil}}}, "A1") + ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{nil}}} + _, err := ws.mergeCellsParser("A1") assert.NoError(t, err) } diff --git a/picture.go b/picture.go index edf53732c1..714f88a52d 100644 --- a/picture.go +++ b/picture.go @@ -216,7 +216,7 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error { if err != nil { return err } - ws.Lock() + ws.mu.Lock() // Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder. drawingID := f.countDrawings() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" @@ -231,7 +231,7 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error { } drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, options.Hyperlink, hyperlinkType) } - ws.Unlock() + ws.mu.Unlock() err = f.addDrawingPicture(sheet, drawingXML, cell, ext, drawingRID, drawingHyperlinkRID, img, options) if err != nil { return err @@ -256,8 +256,8 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { if sheetRels == nil { sheetRels = &xlsxRelationships{} } - sheetRels.Lock() - defer sheetRels.Unlock() + sheetRels.mu.Lock() + defer sheetRels.mu.Unlock() for k, v := range sheetRels.Relationships { if v.ID == rID { sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...) @@ -391,8 +391,8 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper FLocksWithSheet: *opts.Locked, FPrintsWithSheet: *opts.PrintObject, } - content.Lock() - defer content.Unlock() + content.mu.Lock() + defer content.mu.Unlock() content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) f.Drawings.Store(drawingXML, content) return err @@ -447,8 +447,8 @@ func (f *File) setContentTypePartImageExtensions() error { if err != nil { return err } - content.Lock() - defer content.Unlock() + content.mu.Lock() + defer content.mu.Unlock() for _, file := range content.Defaults { delete(imageTypes, file.Extension) } @@ -469,8 +469,8 @@ func (f *File) setContentTypePartVMLExtensions() error { if err != nil { return err } - content.Lock() - defer content.Unlock() + content.mu.Lock() + defer content.mu.Unlock() for _, v := range content.Defaults { if v.Extension == "vml" { vml = true @@ -522,8 +522,8 @@ func (f *File) addContentTypePart(index int, contentType string) error { if err != nil { return err } - content.Lock() - defer content.Unlock() + content.mu.Lock() + defer content.mu.Unlock() for _, v := range content.Overrides { if v.PartName == partNames[contentType] { return err @@ -549,8 +549,8 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { if sheetRels == nil { sheetRels = &xlsxRelationships{} } - sheetRels.Lock() - defer sheetRels.Unlock() + sheetRels.mu.Lock() + defer sheetRels.mu.Unlock() for _, v := range sheetRels.Relationships { if v.ID == rID { return v.Target @@ -683,8 +683,8 @@ func (f *File) getPicturesFromWsDr(row, col int, drawingRelationships string, ws anchor *xdrCellAnchor drawRel *xlsxRelationship ) - wsDr.Lock() - defer wsDr.Unlock() + wsDr.mu.Lock() + defer wsDr.mu.Unlock() for _, anchor = range wsDr.TwoCellAnchor { if anchor.From != nil && anchor.Pic != nil { if anchor.From.Col == col && anchor.From.Row == row { @@ -710,8 +710,8 @@ func (f *File) getPicturesFromWsDr(row, col int, drawingRelationships string, ws // relationship ID. func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship { if drawingRels, _ := f.relsReader(rels); drawingRels != nil { - drawingRels.Lock() - defer drawingRels.Unlock() + drawingRels.mu.Lock() + defer drawingRels.mu.Unlock() for _, v := range drawingRels.Relationships { if v.ID == rID { return &v diff --git a/rows.go b/rows.go index 05257e53e8..c08a56d5bc 100644 --- a/rows.go +++ b/rows.go @@ -267,12 +267,12 @@ func (f *File) Rows(sheet string) (*Rows, error) { if !ok { return nil, ErrSheetNotExist{sheet} } - if ws, ok := f.Sheet.Load(name); ok && ws != nil { - worksheet := ws.(*xlsxWorksheet) - worksheet.Lock() - defer worksheet.Unlock() + if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil { + ws := worksheet.(*xlsxWorksheet) + ws.mu.Lock() + defer ws.mu.Unlock() // Flush data - output, _ := xml.Marshal(worksheet) + output, _ := xml.Marshal(ws) f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } var err error @@ -360,7 +360,7 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { return err } - prepareSheetXML(ws, 0, row) + ws.prepareSheetXML(0, row) rowIdx := row - 1 ws.SheetData.Row[rowIdx].Ht = float64Ptr(height) @@ -372,8 +372,8 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { // name and row number. func (f *File) getRowHeight(sheet string, row int) int { ws, _ := f.workSheetReader(sheet) - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() for i := range ws.SheetData.Row { v := &ws.SheetData.Row[i] if v.R == row && v.Ht != nil { @@ -416,8 +416,8 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { // after deserialization of xl/sharedStrings.xml. func (f *File) sharedStringsReader() (*xlsxSST, error) { var err error - f.Lock() - defer f.Unlock() + f.mu.Lock() + defer f.mu.Unlock() relPath := f.getWorkbookRelsPath() if f.SharedStrings == nil { var sharedStrings xlsxSST @@ -470,7 +470,7 @@ func (f *File) SetRowVisible(sheet string, row int, visible bool) error { if err != nil { return err } - prepareSheetXML(ws, 0, row) + ws.prepareSheetXML(0, row) ws.SheetData.Row[row-1].Hidden = !visible return nil } @@ -511,7 +511,7 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error { if err != nil { return err } - prepareSheetXML(ws, 0, row) + ws.prepareSheetXML(0, row) ws.SheetData.Row[row-1].OutlineLevel = level return nil } @@ -724,7 +724,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in // // Notice: this method could be very slow for large spreadsheets (more than // 3000 rows one sheet). -func checkRow(ws *xlsxWorksheet) error { +func (ws *xlsxWorksheet) checkRow() error { for rowIdx := range ws.SheetData.Row { rowData := &ws.SheetData.Row[rowIdx] @@ -814,8 +814,8 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error { if err != nil { return err } - s.Lock() - defer s.Unlock() + s.mu.Lock() + defer s.mu.Unlock() if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { return newInvalidStyleID(styleID) } @@ -823,7 +823,7 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error { if err != nil { return err } - prepareSheetXML(ws, 0, end) + ws.prepareSheetXML(0, end) for row := start - 1; row < end; row++ { ws.SheetData.Row[row].S = styleID ws.SheetData.Row[row].CustomFormat = true diff --git a/sheet.go b/sheet.go index a022eb0904..d80d057efd 100644 --- a/sheet.go +++ b/sheet.go @@ -70,8 +70,8 @@ func (f *File) NewSheet(sheet string) (int, error) { func (f *File) contentTypesReader() (*xlsxTypes, error) { if f.ContentTypes == nil { f.ContentTypes = new(xlsxTypes) - f.ContentTypes.Lock() - defer f.ContentTypes.Unlock() + f.ContentTypes.mu.Lock() + defer f.ContentTypes.mu.Unlock() if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathContentTypes)))). Decode(f.ContentTypes); err != nil && err != io.EOF { return f.ContentTypes, err @@ -217,8 +217,8 @@ func (f *File) setContentTypes(partName, contentType string) error { if err != nil { return err } - content.Lock() - defer content.Unlock() + content.mu.Lock() + defer content.mu.Unlock() content.Overrides = append(content.Overrides, xlsxOverride{ PartName: partName, ContentType: contentType, @@ -615,8 +615,8 @@ func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) { // relationships by given relationships ID in the file workbook.xml.rels. func (f *File) deleteSheetFromWorkbookRels(rID string) string { rels, _ := f.relsReader(f.getWorkbookRelsPath()) - rels.Lock() - defer rels.Unlock() + rels.mu.Lock() + defer rels.mu.Unlock() for k, v := range rels.Relationships { if v.ID == rID { rels.Relationships = append(rels.Relationships[:k], rels.Relationships[k+1:]...) @@ -636,8 +636,8 @@ func (f *File) deleteSheetFromContentTypes(target string) error { if err != nil { return err } - content.Lock() - defer content.Unlock() + content.mu.Lock() + defer content.mu.Unlock() for k, v := range content.Overrides { if v.PartName == target { content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...) @@ -1842,9 +1842,9 @@ func (f *File) relsReader(path string) (*xlsxRelationships, error) { // fillSheetData ensures there are enough rows, and columns in the chosen // row to accept data. Missing rows are backfilled and given their row number // Uses the last populated row as a hint for the size of the next row to add -func prepareSheetXML(ws *xlsxWorksheet, col int, row int) { - ws.Lock() - defer ws.Unlock() +func (ws *xlsxWorksheet) prepareSheetXML(col int, row int) { + ws.mu.Lock() + defer ws.mu.Unlock() rowCount := len(ws.SheetData.Row) sizeHint := 0 var ht *float64 @@ -1879,8 +1879,8 @@ func fillColumns(rowData *xlsxRow, col, row int) { // makeContiguousColumns make columns in specific rows as contiguous. func makeContiguousColumns(ws *xlsxWorksheet, fromRow, toRow, colCount int) { - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() for ; fromRow < toRow; fromRow++ { rowData := &ws.SheetData.Row[fromRow-1] fillColumns(rowData, colCount, fromRow) diff --git a/styles.go b/styles.go index 27c48aae7a..05f641d8ae 100644 --- a/styles.go +++ b/styles.go @@ -2008,8 +2008,8 @@ func (f *File) NewStyle(style *Style) (int, error) { if err != nil { return cellXfsID, err } - s.Lock() - defer s.Unlock() + s.mu.Lock() + defer s.mu.Unlock() // check given style already exist. if cellXfsID, err = f.getStyleID(s, fs); err != nil || cellXfsID != -1 { return cellXfsID, err @@ -2669,10 +2669,10 @@ func (f *File) GetCellStyle(sheet, cell string) (int, error) { if err != nil { return 0, err } - prepareSheetXML(ws, col, row) - ws.Lock() - defer ws.Unlock() - return f.prepareCellStyle(ws, col, row, ws.SheetData.Row[row-1].C[col-1].S), err + ws.prepareSheetXML(col, row) + ws.mu.Lock() + defer ws.mu.Unlock() + return ws.prepareCellStyle(col, row, ws.SheetData.Row[row-1].C[col-1].S), err } // SetCellStyle provides a function to add style attribute for cells by given @@ -2808,17 +2808,17 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { if err != nil { return err } - prepareSheetXML(ws, vCol, vRow) + ws.prepareSheetXML(vCol, vRow) makeContiguousColumns(ws, hRow, vRow, vCol) - ws.Lock() - defer ws.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() s, err := f.stylesReader() if err != nil { return err } - s.Lock() - defer s.Unlock() + s.mu.Lock() + defer s.mu.Unlock() if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { return newInvalidStyleID(styleID) } diff --git a/workbook.go b/workbook.go index da4e2b1168..c560d5e30a 100644 --- a/workbook.go +++ b/workbook.go @@ -145,8 +145,8 @@ func (f *File) setWorkbook(name string, sheetID, rid int) { // the spreadsheet. func (f *File) getWorkbookPath() (path string) { if rels, _ := f.relsReader("_rels/.rels"); rels != nil { - rels.Lock() - defer rels.Unlock() + rels.mu.Lock() + defer rels.mu.Unlock() for _, rel := range rels.Relationships { if rel.Type == SourceRelationshipOfficeDocument { path = strings.TrimPrefix(rel.Target, "/") diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 950c09b68b..ee13069dfa 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -20,7 +20,7 @@ import ( // parts, it takes a Multipurpose Internet Mail Extension (MIME) media type as a // value. type xlsxTypes struct { - sync.Mutex + mu sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/content-types Types"` Defaults []xlsxDefault `xml:"Default"` Overrides []xlsxOverride `xml:"Override"` diff --git a/xmlDrawing.go b/xmlDrawing.go index caf9897ae3..9e7c48ea31 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -440,7 +440,7 @@ type xlsxPoint2D struct { // xlsxWsDr directly maps the root element for a part of this content type shall // wsDr. type xlsxWsDr struct { - sync.Mutex + mu sync.Mutex XMLName xml.Name `xml:"xdr:wsDr"` A string `xml:"xmlns:a,attr,omitempty"` Xdr string `xml:"xmlns:xdr,attr,omitempty"` diff --git a/xmlStyles.go b/xmlStyles.go index 070036cae5..437446ebed 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -18,7 +18,7 @@ import ( // xlsxStyleSheet is the root element of the Styles part. type xlsxStyleSheet struct { - sync.Mutex + mu sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main styleSheet"` NumFmts *xlsxNumFmts `xml:"numFmts"` Fonts *xlsxFonts `xml:"fonts"` diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 4bcb5f6d11..bc71bd4c9e 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -18,7 +18,7 @@ import ( // xlsxRelationships describe references from parts to other internal resources in the package or to external resources. type xlsxRelationships struct { - sync.Mutex + mu sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"` Relationships []xlsxRelationship `xml:"Relationship"` } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 8e89761860..f23c4142f6 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -19,7 +19,7 @@ import ( // xlsxWorksheet directly maps the worksheet element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main. type xlsxWorksheet struct { - sync.Mutex + mu sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` SheetPr *xlsxSheetPr `xml:"sheetPr"` Dimension *xlsxDimension `xml:"dimension"` From 612f6f104c899be339c1f9b6a408d54b0847234f Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 25 Apr 2023 08:44:41 +0800 Subject: [PATCH 188/213] This closes #1528, closes #1533 - Avoid format text cell value as a numeric - Fix race conditions for concurrency safety functions --- cell.go | 39 +++++++++++++++++++++++++-------------- cell_test.go | 2 +- col.go | 24 ++++++++++++++++++++---- excelize.go | 2 -- numfmt.go | 5 ++++- numfmt_test.go | 8 ++++++++ picture.go | 6 ++++++ sheet.go | 6 +----- styles.go | 29 +++++++++++++++++++++-------- 9 files changed, 86 insertions(+), 35 deletions(-) diff --git a/cell.go b/cell.go index 229267fc8c..f455edff46 100644 --- a/cell.go +++ b/cell.go @@ -288,16 +288,19 @@ func setCellDuration(value time.Duration) (t string, v string) { // SetCellInt provides a function to set int type value of a cell by given // worksheet name, cell reference and cell value. func (f *File) SetCellInt(sheet, cell string, value int) error { + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return err } + f.mu.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() c, col, row, err := ws.prepareCell(cell) if err != nil { return err } - ws.mu.Lock() - defer ws.mu.Unlock() c.S = ws.prepareCellStyle(col, row, c.S) c.T, c.V = setCellInt(value) c.IS = nil @@ -314,16 +317,19 @@ func setCellInt(value int) (t string, v string) { // SetCellBool provides a function to set bool type value of a cell by given // worksheet name, cell reference and cell value. func (f *File) SetCellBool(sheet, cell string, value bool) error { + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return err } + f.mu.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() c, col, row, err := ws.prepareCell(cell) if err != nil { return err } - ws.mu.Lock() - defer ws.mu.Unlock() c.S = ws.prepareCellStyle(col, row, c.S) c.T, c.V = setCellBool(value) c.IS = nil @@ -351,16 +357,19 @@ func setCellBool(value bool) (t string, v string) { // var x float32 = 1.325 // f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSize int) error { + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return err } + f.mu.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() c, col, row, err := ws.prepareCell(cell) if err != nil { return err } - ws.mu.Lock() - defer ws.mu.Unlock() c.S = ws.prepareCellStyle(col, row, c.S) c.T, c.V = setCellFloat(value, precision, bitSize) c.IS = nil @@ -377,16 +386,19 @@ func setCellFloat(value float64, precision, bitSize int) (t string, v string) { // SetCellStr provides a function to set string type value of a cell. Total // number of characters that a cell can contain 32767 characters. func (f *File) SetCellStr(sheet, cell, value string) error { + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return err } + f.mu.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() c, col, row, err := ws.prepareCell(cell) if err != nil { return err } - ws.mu.Lock() - defer ws.mu.Unlock() c.S = ws.prepareCellStyle(col, row, c.S) if c.T, c.V, err = f.setCellString(value); err != nil { return err @@ -558,8 +570,6 @@ func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) { // intended to be used with for range on rows an argument with the spreadsheet // opened file. func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { - f.mu.Lock() - defer f.mu.Unlock() switch c.T { case "b": return c.getCellBool(f, raw) @@ -596,16 +606,19 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { // SetCellDefault provides a function to set string type value of a cell as // default format without escaping the cell. func (f *File) SetCellDefault(sheet, cell, value string) error { + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return err } + f.mu.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() c, col, row, err := ws.prepareCell(cell) if err != nil { return err } - ws.mu.Lock() - defer ws.mu.Unlock() c.S = ws.prepareCellStyle(col, row, c.S) c.setCellDefault(value) return f.removeFormula(c, ws, sheet) @@ -1264,8 +1277,6 @@ func (ws *xlsxWorksheet) prepareCell(cell string) (*xlsxC, int, int, error) { } ws.prepareSheetXML(col, row) - ws.mu.Lock() - defer ws.mu.Unlock() return &ws.SheetData.Row[row-1].C[col-1], col, row, err } diff --git a/cell_test.go b/cell_test.go index b395478805..89ec1733ac 100644 --- a/cell_test.go +++ b/cell_test.go @@ -65,7 +65,7 @@ func TestConcurrency(t *testing.T) { // Concurrency iterate columns cols, err := f.Cols("Sheet1") assert.NoError(t, err) - for rows.Next() { + for cols.Next() { _, err := cols.Rows() assert.NoError(t, err) } diff --git a/col.go b/col.go index dd50fb4793..e396005b4f 100644 --- a/col.go +++ b/col.go @@ -257,10 +257,13 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { if err != nil { return true, err } + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return false, err } + f.mu.Unlock() ws.mu.Lock() defer ws.mu.Unlock() if ws.Cols == nil { @@ -428,20 +431,24 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { if err != nil { return err } + f.mu.Lock() s, err := f.stylesReader() if err != nil { + f.mu.Unlock() return err } + ws, err := f.workSheetReader(sheet) + if err != nil { + f.mu.Unlock() + return err + } + f.mu.Unlock() s.mu.Lock() if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { s.mu.Unlock() return newInvalidStyleID(styleID) } s.mu.Unlock() - ws, err := f.workSheetReader(sheet) - if err != nil { - return err - } ws.mu.Lock() if ws.Cols == nil { ws.Cols = &xlsxCols{} @@ -484,10 +491,13 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error if width > MaxColumnWidth { return ErrColumnWidth } + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return err } + f.mu.Unlock() ws.mu.Lock() defer ws.mu.Unlock() col := xlsxCol{ @@ -659,10 +669,13 @@ func (f *File) GetColStyle(sheet, col string) (int, error) { if err != nil { return styleID, err } + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return styleID, err } + f.mu.Unlock() ws.mu.Lock() defer ws.mu.Unlock() if ws.Cols != nil { @@ -682,10 +695,13 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { if err != nil { return defaultColWidth, err } + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return defaultColWidth, err } + f.mu.Unlock() ws.mu.Lock() defer ws.mu.Unlock() if ws.Cols != nil { diff --git a/excelize.go b/excelize.go index 2c75f1f0ce..6c7ce2a181 100644 --- a/excelize.go +++ b/excelize.go @@ -234,8 +234,6 @@ func (f *File) setDefaultTimeStyle(sheet, cell string, format int) error { // workSheetReader provides a function to get the pointer to the structure // after deserialization by given worksheet name. func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { - f.mu.Lock() - defer f.mu.Unlock() var ( name string ok bool diff --git a/numfmt.go b/numfmt.go index b5c81bf833..cad67a2d4d 100644 --- a/numfmt.go +++ b/numfmt.go @@ -948,10 +948,13 @@ func (nf *numberFormat) zeroHandler() string { // textHandler will be handling text selection for a number format expression. func (nf *numberFormat) textHandler() (result string) { for _, token := range nf.section[nf.sectionIdx].Items { + if inStrSlice([]string{nfp.TokenTypeDateTimes, nfp.TokenTypeElapsedDateTimes}, token.TType, false) != -1 { + return nf.value + } if token.TType == nfp.TokenTypeLiteral { result += token.TValue } - if token.TType == nfp.TokenTypeTextPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder { + if token.TType == nfp.TokenTypeGeneral || token.TType == nfp.TokenTypeTextPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder { result += nf.value } } diff --git a/numfmt_test.go b/numfmt_test.go index 51ee8e21f0..773fac32e7 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -1008,4 +1008,12 @@ func TestNumFmt(t *testing.T) { result := format(item[0], item[1], false, CellTypeNumber) assert.Equal(t, item[2], result, item) } + for _, item := range [][]string{ + {"1234.5678", "General", "1234.5678"}, + {"1234.5678", "yyyy\"年\"m\"月\"d\"日\";@", "1234.5678"}, + {"1234.5678", "0_);[Red]\\(0\\)", "1234.5678"}, + } { + result := format(item[0], item[1], false, CellTypeSharedString) + assert.Equal(t, item[2], result, item) + } } diff --git a/picture.go b/picture.go index 714f88a52d..6ee83595f4 100644 --- a/picture.go +++ b/picture.go @@ -212,10 +212,13 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error { return err } // Read sheet data. + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return err } + f.mu.Unlock() ws.mu.Lock() // Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder. drawingID := f.countDrawings() + 1 @@ -591,10 +594,13 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) { } col-- row-- + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return nil, err } + f.mu.Unlock() if ws.Drawing == nil { return nil, err } diff --git a/sheet.go b/sheet.go index d80d057efd..8d441dd4f9 100644 --- a/sheet.go +++ b/sheet.go @@ -1843,8 +1843,6 @@ func (f *File) relsReader(path string) (*xlsxRelationships, error) { // row to accept data. Missing rows are backfilled and given their row number // Uses the last populated row as a hint for the size of the next row to add func (ws *xlsxWorksheet) prepareSheetXML(col int, row int) { - ws.mu.Lock() - defer ws.mu.Unlock() rowCount := len(ws.SheetData.Row) sizeHint := 0 var ht *float64 @@ -1878,9 +1876,7 @@ func fillColumns(rowData *xlsxRow, col, row int) { } // makeContiguousColumns make columns in specific rows as contiguous. -func makeContiguousColumns(ws *xlsxWorksheet, fromRow, toRow, colCount int) { - ws.mu.Lock() - defer ws.mu.Unlock() +func (ws *xlsxWorksheet) makeContiguousColumns(fromRow, toRow, colCount int) { for ; fromRow < toRow; fromRow++ { rowData := &ws.SheetData.Row[fromRow-1] fillColumns(rowData, colCount, fromRow) diff --git a/styles.go b/styles.go index 05f641d8ae..db7b5609fc 100644 --- a/styles.go +++ b/styles.go @@ -2004,10 +2004,13 @@ func (f *File) NewStyle(style *Style) (int, error) { if fs.DecimalPlaces == 0 { fs.DecimalPlaces = 2 } + f.mu.Lock() s, err := f.stylesReader() if err != nil { + f.mu.Unlock() return cellXfsID, err } + f.mu.Unlock() s.mu.Lock() defer s.mu.Unlock() // check given style already exist. @@ -2131,10 +2134,13 @@ func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (int, error) { // format by given style format. The parameters are the same with the NewStyle // function. func (f *File) NewConditionalStyle(style *Style) (int, error) { + f.mu.Lock() s, err := f.stylesReader() if err != nil { + f.mu.Unlock() return 0, err } + f.mu.Unlock() fs, err := parseFormatStyleSet(style) if err != nil { return 0, err @@ -2179,7 +2185,9 @@ func (f *File) SetDefaultFont(fontName string) error { return err } font.Name.Val = stringPtr(fontName) + f.mu.Lock() s, _ := f.stylesReader() + f.mu.Unlock() s.Fonts.Font[0] = font custom := true s.CellStyles.CellStyle[0].CustomBuiltIn = &custom @@ -2188,6 +2196,8 @@ func (f *File) SetDefaultFont(fontName string) error { // readDefaultFont provides an un-marshalled font value. func (f *File) readDefaultFont() (*xlsxFont, error) { + f.mu.Lock() + defer f.mu.Unlock() s, err := f.stylesReader() if err != nil { return nil, err @@ -2803,22 +2813,25 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { vColIdx := vCol - 1 vRowIdx := vRow - 1 - + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return err } - ws.prepareSheetXML(vCol, vRow) - makeContiguousColumns(ws, hRow, vRow, vCol) - ws.mu.Lock() - defer ws.mu.Unlock() - s, err := f.stylesReader() if err != nil { + f.mu.Unlock() return err } - s.mu.Lock() - defer s.mu.Unlock() + f.mu.Unlock() + + ws.mu.Lock() + defer ws.mu.Unlock() + + ws.prepareSheetXML(vCol, vRow) + ws.makeContiguousColumns(hRow, vRow, vCol) + if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { return newInvalidStyleID(styleID) } From 65fc25e7a60c096cea25e16d354d67b327596889 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 26 Apr 2023 00:04:47 +0800 Subject: [PATCH 189/213] Ref #1533, this made number format text handler just handle text tokens - Fix race conditions for concurrency read and write shared string table - Unit tests has been updated --- cell.go | 4 ++++ numfmt.go | 8 ++++---- numfmt_test.go | 19 ++++++++++++------- xmlSharedStrings.go | 6 +++++- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/cell.go b/cell.go index f455edff46..61dbed6216 100644 --- a/cell.go +++ b/cell.go @@ -460,6 +460,8 @@ func (f *File) setSharedString(val string) (int, error) { if i, ok := f.sharedStringsMap[val]; ok { return i, nil } + sst.mu.Lock() + defer sst.mu.Unlock() sst.Count++ sst.UniqueCount++ t := xlsxT{Val: val} @@ -581,6 +583,8 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { return f.formattedValue(&xlsxC{S: c.S, V: f.getFromStringItem(xlsxSI)}, raw, CellTypeSharedString) } + d.mu.Lock() + defer d.mu.Unlock() if len(d.SI) > xlsxSI { return f.formattedValue(&xlsxC{S: c.S, V: d.SI[xlsxSI].String()}, raw, CellTypeSharedString) } diff --git a/numfmt.go b/numfmt.go index cad67a2d4d..29702302a4 100644 --- a/numfmt.go +++ b/numfmt.go @@ -948,13 +948,10 @@ func (nf *numberFormat) zeroHandler() string { // textHandler will be handling text selection for a number format expression. func (nf *numberFormat) textHandler() (result string) { for _, token := range nf.section[nf.sectionIdx].Items { - if inStrSlice([]string{nfp.TokenTypeDateTimes, nfp.TokenTypeElapsedDateTimes}, token.TType, false) != -1 { - return nf.value - } if token.TType == nfp.TokenTypeLiteral { result += token.TValue } - if token.TType == nfp.TokenTypeGeneral || token.TType == nfp.TokenTypeTextPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder { + if token.TType == nfp.TokenTypeTextPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder { result += nf.value } } @@ -964,6 +961,9 @@ func (nf *numberFormat) textHandler() (result string) { // getValueSectionType returns its applicable number format expression section // based on the given value. func (nf *numberFormat) getValueSectionType(value string) (float64, string) { + if nf.cellType != CellTypeNumber && nf.cellType != CellTypeDate { + return 0, nfp.TokenSectionText + } isNum, _, _ := isNumeric(value) if !isNum { return 0, nfp.TokenSectionText diff --git a/numfmt_test.go b/numfmt_test.go index 773fac32e7..1e6f6bb86a 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -1008,12 +1008,17 @@ func TestNumFmt(t *testing.T) { result := format(item[0], item[1], false, CellTypeNumber) assert.Equal(t, item[2], result, item) } - for _, item := range [][]string{ - {"1234.5678", "General", "1234.5678"}, - {"1234.5678", "yyyy\"年\"m\"月\"d\"日\";@", "1234.5678"}, - {"1234.5678", "0_);[Red]\\(0\\)", "1234.5678"}, - } { - result := format(item[0], item[1], false, CellTypeSharedString) - assert.Equal(t, item[2], result, item) + for _, cellType := range []CellType{CellTypeSharedString, CellTypeInlineString} { + for _, item := range [][]string{ + {"1234.5678", "General", "1234.5678"}, + {"1234.5678", "yyyy\"年\"m\"月\"d\"日\";@", "1234.5678"}, + {"1234.5678", "h\"时\"mm\"分\"ss\"秒\";@", "1234.5678"}, + {"1234.5678", "\"¥\"#,##0.00_);\\(\"¥\"#,##0.00\\)", "1234.5678"}, + {"1234.5678", "0_);[Red]\\(0\\)", "1234.5678"}, + {"1234.5678", "\"text\"@", "text1234.5678"}, + } { + result := format(item[0], item[1], false, cellType) + assert.Equal(t, item[2], result, item) + } } } diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 704002c7fb..b2b65d1efa 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -11,7 +11,10 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" + "sync" +) // xlsxSST directly maps the sst element from the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main. String values may @@ -21,6 +24,7 @@ import "encoding/xml" // is an indexed list of string values, shared across the workbook, which allows // implementations to store values only once. type xlsxSST struct { + mu sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main sst"` Count int `xml:"count,attr"` UniqueCount int `xml:"uniqueCount,attr"` From 7c221cf29531fcd38871d3295f4b511029cb4282 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 30 Apr 2023 11:10:51 +0800 Subject: [PATCH 190/213] Ref #660, support placeholder, padding and rounds numbers by specified number format code - Remove built-in number formats functions - Update unit tests - Upgrade dependencies package --- cell.go | 4 +- cell_test.go | 8 +- excelize_test.go | 35 +++--- go.mod | 4 +- go.sum | 8 +- numfmt.go | 305 +++++++++++++++++++++++++++++++++++++++-------- numfmt_test.go | 34 ++++++ rows_test.go | 2 +- styles.go | 211 +------------------------------- 9 files changed, 321 insertions(+), 290 deletions(-) diff --git a/cell.go b/cell.go index 61dbed6216..2ab05dc3ca 100644 --- a/cell.go +++ b/cell.go @@ -1365,8 +1365,8 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er if wb != nil && wb.WorkbookPr != nil { date1904 = wb.WorkbookPr.Date1904 } - if ok := builtInNumFmtFunc[numFmtID]; ok != nil { - return ok(c.V, builtInNumFmt[numFmtID], date1904, cellType), err + if fmtCode, ok := builtInNumFmt[numFmtID]; ok { + return format(c.V, fmtCode, date1904, cellType), err } if styleSheet.NumFmts == nil { return c.V, err diff --git a/cell_test.go b/cell_test.go index 89ec1733ac..ec7e5a32f0 100644 --- a/cell_test.go +++ b/cell_test.go @@ -873,9 +873,7 @@ func TestFormattedValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "311", result) - for _, fn := range builtInNumFmtFunc { - assert.Equal(t, "0_0", fn("0_0", "", false, CellTypeNumber)) - } + assert.Equal(t, "0_0", format("0_0", "", false, CellTypeNumber)) // Test format value with unsupported charset workbook f.WorkBook = nil @@ -889,9 +887,7 @@ func TestFormattedValue(t *testing.T) { _, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") - for _, fn := range builtInNumFmtFunc { - assert.Equal(t, fn("text", "0", false, CellTypeNumber), "text") - } + assert.Equal(t, "text", format("text", "0", false, CellTypeNumber)) } func TestFormattedValueNilXfs(t *testing.T) { diff --git a/excelize_test.go b/excelize_test.go index 17d16f0d4b..59ce3dfc42 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -747,33 +747,33 @@ func TestSetCellStyleNumberFormat(t *testing.T) { // Test only set fill and number format for a cell col := []string{"L", "M", "N", "O", "P"} - data := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49} + idxTbl := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49} value := []string{"37947.7500001", "-37947.7500001", "0.007", "2.1", "String"} expected := [][]string{ - {"37947.7500001", "37948", "37947.75", "37,948", "37947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 pm", "6:00:00 pm", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "37947.7500001", "3.79E+04", "37947.7500001"}, - {"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-3.79E+04", "-37947.7500001"}, - {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 am", "0:10:04 am", "00:10", "00:10:04", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:04", "0:10:04", "0.007", "7.00E-03", "0.007"}, - {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 am", "2:24:00 am", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "2.1", "2.10E+00", "2.1"}, + {"37947.7500001", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 pm", "6:00:00 pm", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "0000.0", "37947.7500001", "37947.7500001"}, + {"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37,947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001"}, + {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 am", "0:10:04 am", "00:10", "00:10:04", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:04", "0:10:04", "1004.0", "0.007", "0.007"}, + {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 am", "2:24:00 am", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "2400.0", "2.1", "2.1"}, {"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String"}, } - for i, v := range value { - for k, d := range data { - c := col[i] + strconv.Itoa(k+1) + for c, v := range value { + for r, idx := range idxTbl { + cell := col[c] + strconv.Itoa(r+1) var val float64 val, err = strconv.ParseFloat(v, 64) if err != nil { - assert.NoError(t, f.SetCellValue("Sheet2", c, v)) + assert.NoError(t, f.SetCellValue("Sheet2", cell, v)) } else { - assert.NoError(t, f.SetCellValue("Sheet2", c, val)) + assert.NoError(t, f.SetCellValue("Sheet2", cell, val)) } - style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 5}, NumFmt: d}) + style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 5}, NumFmt: idx}) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, f.SetCellStyle("Sheet2", c, c, style)) - cellValue, err := f.GetCellValue("Sheet2", c) - assert.Equal(t, expected[i][k], cellValue, fmt.Sprintf("Sheet2!%s value: %s, number format: %d", c, value[i], k)) + assert.NoError(t, f.SetCellStyle("Sheet2", cell, cell, style)) + cellValue, err := f.GetCellValue("Sheet2", cell) + assert.Equal(t, expected[c][r], cellValue, fmt.Sprintf("Sheet2!%s value: %s, number format: %s c: %d r: %d", cell, value[c], builtInNumFmt[idx], c, r)) assert.NoError(t, err) } } @@ -997,7 +997,7 @@ func TestConditionalFormat(t *testing.T) { f := NewFile() sheet1 := f.GetSheetName(0) - fillCells(f, sheet1, 10, 15) + assert.NoError(t, fillCells(f, sheet1, 10, 15)) var format1, format2, format3, format4 int var err error @@ -1612,15 +1612,16 @@ func prepareTestBook4() (*File, error) { return f, nil } -func fillCells(f *File, sheet string, colCount, rowCount int) { +func fillCells(f *File, sheet string, colCount, rowCount int) error { for col := 1; col <= colCount; col++ { for row := 1; row <= rowCount; row++ { cell, _ := CoordinatesToCellName(col, row) if err := f.SetCellStr(sheet, cell, cell); err != nil { - fmt.Println(err) + return err } } } + return nil } func BenchmarkOpenFile(b *testing.B) { diff --git a/go.mod b/go.mod index 12b024e54a..a266969a31 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.8.0 - github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 - github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 + github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 + github.com/xuri/nfp v0.0.0-20230428090735-b50b0f0358f4 golang.org/x/crypto v0.8.0 golang.org/x/image v0.5.0 golang.org/x/net v0.9.0 diff --git a/go.sum b/go.sum index 3c5a9eb918..c57411bd7f 100644 --- a/go.sum +++ b/go.sum @@ -15,10 +15,10 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -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= +github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E= +github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/nfp v0.0.0-20230428090735-b50b0f0358f4 h1:YoU/1S7L25dvNepEir3Fg2aU9iGmDyE4gWKoEswWXts= +github.com/xuri/nfp v0.0.0-20230428090735-b50b0f0358f4/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= diff --git a/numfmt.go b/numfmt.go index 29702302a4..5f5f180fe4 100644 --- a/numfmt.go +++ b/numfmt.go @@ -21,7 +21,8 @@ import ( "github.com/xuri/nfp" ) -// languageInfo defined the required fields of localization support for number format. +// languageInfo defined the required fields of localization support for number +// format. type languageInfo struct { apFmt string tags []string @@ -31,13 +32,16 @@ type languageInfo struct { // numberFormat directly maps the number format parser runtime required // fields. type numberFormat struct { - cellType CellType - section []nfp.Section - t time.Time - sectionIdx int - date1904, isNumeric, hours, seconds bool - number float64 - ap, localCode, result, value, valueSectionType string + cellType CellType + section []nfp.Section + t time.Time + sectionIdx int + date1904, isNumeric, hours, seconds bool + number float64 + ap, localCode, result, value, valueSectionType string + fracHolder, fracPadding, intHolder, intPadding, expBaseLen int + percent int + useCommaSep, usePointer, usePositive, useScientificNotation bool } var ( @@ -47,12 +51,33 @@ var ( nfp.TokenTypeColor, nfp.TokenTypeCurrencyLanguage, nfp.TokenTypeDateTimes, + nfp.TokenTypeDecimalPoint, nfp.TokenTypeElapsedDateTimes, + nfp.TokenTypeExponential, nfp.TokenTypeGeneral, + nfp.TokenTypeHashPlaceHolder, nfp.TokenTypeLiteral, + nfp.TokenTypePercent, nfp.TokenTypeTextPlaceHolder, + nfp.TokenTypeThousandsSeparator, nfp.TokenTypeZeroPlaceHolder, } + // supportedNumberTokenTypes list the supported number token types. + supportedNumberTokenTypes = []string{ + nfp.TokenTypeColor, + nfp.TokenTypeDecimalPoint, + nfp.TokenTypeHashPlaceHolder, + nfp.TokenTypeLiteral, + nfp.TokenTypePercent, + nfp.TokenTypeThousandsSeparator, + nfp.TokenTypeZeroPlaceHolder, + } + // supportedDateTimeTokenTypes list the supported date and time token types. + supportedDateTimeTokenTypes = []string{ + nfp.TokenTypeCurrencyLanguage, + nfp.TokenTypeDateTimes, + nfp.TokenTypeElapsedDateTimes, + } // supportedLanguageInfo directly maps the supported language ID and tags. supportedLanguageInfo = map[string]languageInfo{ "36": {tags: []string{"af"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans}, @@ -373,15 +398,172 @@ func format(value, numFmt string, date1904 bool, cellType CellType) string { return value } -// positiveHandler will be handling positive selection for a number format -// expression. -func (nf *numberFormat) positiveHandler() (result string) { +// getNumberPartLen returns the length of integer and fraction parts for the +// numeric. +func getNumberPartLen(n float64) (int, int) { + parts := strings.Split(strconv.FormatFloat(math.Abs(n), 'f', -1, 64), ".") + if len(parts) == 2 { + return len(parts[0]), len(parts[1]) + } + return len(parts[0]), 0 +} + +// getNumberFmtConf generate the number format padding and place holder +// configurations. +func (nf *numberFormat) getNumberFmtConf() { + for _, token := range nf.section[nf.sectionIdx].Items { + if token.TType == nfp.TokenTypeHashPlaceHolder { + if nf.usePointer { + nf.fracHolder += len(token.TValue) + } else { + nf.intHolder += len(token.TValue) + } + } + if token.TType == nfp.TokenTypeExponential { + nf.useScientificNotation = true + } + if token.TType == nfp.TokenTypeThousandsSeparator { + nf.useCommaSep = true + } + if token.TType == nfp.TokenTypePercent { + nf.percent += len(token.TValue) + } + if token.TType == nfp.TokenTypeDecimalPoint { + nf.usePointer = true + } + if token.TType == nfp.TokenTypeZeroPlaceHolder { + if nf.usePointer { + if nf.useScientificNotation { + nf.expBaseLen += len(token.TValue) + continue + } + nf.fracPadding += len(token.TValue) + continue + } + nf.intPadding += len(token.TValue) + } + } +} + +// printNumberLiteral apply literal tokens for the pre-formatted text. +func (nf *numberFormat) printNumberLiteral(text string) string { + var result string + var useZeroPlaceHolder bool + if nf.usePositive { + result += "-" + } + for _, token := range nf.section[nf.sectionIdx].Items { + if token.TType == nfp.TokenTypeLiteral { + result += token.TValue + } + if !useZeroPlaceHolder && token.TType == nfp.TokenTypeZeroPlaceHolder { + useZeroPlaceHolder = true + result += text + } + } + return result +} + +// printCommaSep format number with thousands separator. +func printCommaSep(text string) string { + var ( + target strings.Builder + subStr = strings.Split(text, ".") + length = len(subStr[0]) + ) + for i := 0; i < length; i++ { + if i > 0 && (length-i)%3 == 0 { + target.WriteString(",") + } + target.WriteString(string(text[i])) + } + if len(subStr) == 2 { + target.WriteString(".") + target.WriteString(subStr[1]) + } + return target.String() +} + +// printBigNumber format number which precision great than 15 with fraction +// zero padding and percentage symbol. +func (nf *numberFormat) printBigNumber(decimal float64, fracLen int) string { + var exp float64 + if nf.percent > 0 { + exp = 1 + } + result := strings.TrimLeft(strconv.FormatFloat(decimal*math.Pow(100, exp), 'f', -1, 64), "-") + if nf.useCommaSep { + result = printCommaSep(result) + } + if fracLen > 0 { + if parts := strings.Split(result, "."); len(parts) == 2 { + fracPartLen := len(parts[1]) + if fracPartLen < fracLen { + result = fmt.Sprintf("%s%s", result, strings.Repeat("0", fracLen-fracPartLen)) + } + if fracPartLen > fracLen { + result = fmt.Sprintf("%s.%s", parts[0], parts[1][:fracLen]) + } + } else { + result = fmt.Sprintf("%s.%s", result, strings.Repeat("0", fracLen)) + } + } + if nf.percent > 0 { + return fmt.Sprintf("%s%%", result) + } + return result +} + +// numberHandler handling number format expression for positive and negative +// numeric. +func (nf *numberFormat) numberHandler() string { + var ( + num = nf.number + intPart, fracPart = getNumberPartLen(nf.number) + intLen, fracLen int + result string + ) + nf.getNumberFmtConf() + if intLen = intPart; nf.intPadding > intPart { + intLen = nf.intPadding + } + if fracLen = fracPart; fracPart > nf.fracHolder+nf.fracPadding { + fracLen = nf.fracHolder + nf.fracPadding + } + if nf.fracPadding > fracPart { + fracLen = nf.fracPadding + } + if isNum, precision, decimal := isNumeric(nf.value); isNum { + if precision > 15 && intLen+fracLen > 15 { + return nf.printNumberLiteral(nf.printBigNumber(decimal, fracLen)) + } + } + paddingLen := intLen + fracLen + if fracLen > 0 { + paddingLen++ + } + flag := "f" + if nf.useScientificNotation { + if nf.expBaseLen != 2 { + return nf.value + } + flag = "E" + } + fmtCode := fmt.Sprintf("%%0%d.%d%s%s", paddingLen, fracLen, flag, strings.Repeat("%%", nf.percent)) + if nf.percent > 0 { + num *= math.Pow(100, float64(nf.percent)) + } + if result = fmt.Sprintf(fmtCode, math.Abs(num)); nf.useCommaSep { + result = printCommaSep(result) + } + return nf.printNumberLiteral(result) +} + +// dateTimeHandler handling data and time number format expression for a +// positive numeric. +func (nf *numberFormat) dateTimeHandler() (result string) { nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false for i, token := range nf.section[nf.sectionIdx].Items { - if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { - result = nf.value - return - } if token.TType == nfp.TokenTypeCurrencyLanguage { if err := nf.currencyLanguageHandler(i, token); err != nil { result = nf.value @@ -398,27 +580,46 @@ func (nf *numberFormat) positiveHandler() (result string) { nf.result += token.TValue continue } - if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) { - if isNum, precision, decimal := isNumeric(nf.value); isNum { - if nf.number < 1 { - nf.result += "0" - continue - } - if precision > 15 { - nf.result += strconv.FormatFloat(decimal, 'f', -1, 64) - } else { - nf.result += fmt.Sprintf("%.f", nf.number) - } - continue + if token.TType == nfp.TokenTypeDecimalPoint { + nf.result += "." + } + if token.TType == nfp.TokenTypeZeroPlaceHolder { + zeroHolderLen := len(token.TValue) + if zeroHolderLen > 3 { + zeroHolderLen = 3 } + nf.result += strings.Repeat("0", zeroHolderLen) } } - result = nf.result - return + return nf.result } -// currencyLanguageHandler will be handling currency and language types tokens for a number -// format expression. +// positiveHandler will be handling positive selection for a number format +// expression. +func (nf *numberFormat) positiveHandler() string { + var fmtNum bool + for _, token := range nf.section[nf.sectionIdx].Items { + if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { + return nf.value + } + if inStrSlice(supportedNumberTokenTypes, token.TType, true) != -1 { + fmtNum = true + } + if inStrSlice(supportedDateTimeTokenTypes, token.TType, true) != -1 { + if fmtNum || nf.number < 0 { + return nf.value + } + return nf.dateTimeHandler() + } + } + if fmtNum { + return nf.numberHandler() + } + return nf.value +} + +// currencyLanguageHandler will be handling currency and language types tokens +// for a number format expression. func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (err error) { for _, part := range token.Parts { if inStrSlice(supportedTokenTypes, part.Token.TType, true) == -1 { @@ -566,7 +767,8 @@ func localMonthsNameKorean(t time.Time, abbr int) string { return strconv.Itoa(int(t.Month())) } -// localMonthsNameTraditionalMongolian returns the Traditional Mongolian name of the month. +// localMonthsNameTraditionalMongolian returns the Traditional Mongolian name of +// the month. func localMonthsNameTraditionalMongolian(t time.Time, abbr int) string { if abbr == 5 { return "M" @@ -912,32 +1114,23 @@ func (nf *numberFormat) secondsNext(i int) bool { // negativeHandler will be handling negative selection for a number format // expression. func (nf *numberFormat) negativeHandler() (result string) { + fmtNum := true for _, token := range nf.section[nf.sectionIdx].Items { if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { - result = nf.value - return + return nf.value } - if token.TType == nfp.TokenTypeLiteral { - nf.result += token.TValue + if inStrSlice(supportedNumberTokenTypes, token.TType, true) != -1 { continue } - if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) { - if isNum, precision, decimal := isNumeric(nf.value); isNum { - if math.Abs(nf.number) < 1 { - nf.result += "0" - continue - } - if precision > 15 { - nf.result += strings.TrimLeft(strconv.FormatFloat(decimal, 'f', -1, 64), "-") - } else { - nf.result += fmt.Sprintf("%.f", math.Abs(nf.number)) - } - continue - } + if inStrSlice(supportedDateTimeTokenTypes, token.TType, true) != -1 { + return nf.value } + fmtNum = false } - result = nf.result - return + if fmtNum { + return nf.numberHandler() + } + return nf.value } // zeroHandler will be handling zero selection for a number format expression. @@ -973,6 +1166,16 @@ func (nf *numberFormat) getValueSectionType(value string) (float64, string) { return number, nfp.TokenSectionPositive } if number < 0 { + var hasNeg bool + for _, sec := range nf.section { + if sec.Type == nfp.TokenSectionNegative { + hasNeg = true + } + } + if !hasNeg { + nf.usePositive = true + return number, nfp.TokenSectionPositive + } return number, nfp.TokenSectionNegative } return number, nfp.TokenSectionZero diff --git a/numfmt_test.go b/numfmt_test.go index 1e6f6bb86a..bf5cbd280f 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -1004,6 +1004,40 @@ func TestNumFmt(t *testing.T) { {"-8.0450685976001E+21", "0_);[Red]\\(0\\)", "(8045068597600100000000)"}, {"-8.0450685976001E-21", "0_);[Red]\\(0\\)", "(0)"}, {"-8.04506", "0_);[Red]\\(0\\)", "(8)"}, + {"1234.5678", "0", "1235"}, + {"1234.5678", "0.00", "1234.57"}, + {"1234.5678", "#,##0", "1,235"}, + {"1234.5678", "#,##0.00", "1,234.57"}, + {"1234.5678", "0%", "123457%"}, + {"1234.5678", "#,##0 ;(#,##0)", "1,235 "}, + {"1234.5678", "#,##0 ;[red](#,##0)", "1,235 "}, + {"1234.5678", "#,##0.00;(#,##0.00)", "1,234.57"}, + {"1234.5678", "#,##0.00;[red](#,##0.00)", "1,234.57"}, + {"-1234.5678", "0.00", "-1234.57"}, + {"-1234.5678", "0.00;-0.00", "-1234.57"}, + {"-1234.5678", "0.00%%", "-12345678.00%%"}, + {"2.1", "mmss.0000", "2400.000"}, + {"1234.5678", "0.00###", "1234.5678"}, + {"1234.5678", "00000.00###", "01234.5678"}, + {"-1234.5678", "00000.00###;;", ""}, + {"1234.5678", "0.00000", "1234.56780"}, + {"8.8888666665555487", "0.00000", "8.88887"}, + {"8.8888666665555493e+19", "#,000.00", "88,888,666,665,555,500,000.00"}, + {"8.8888666665555493e+19", "0.00000", "88888666665555500000.00000"}, + {"37947.7500001", "0.00000000E+00", "3.79477500E+04"}, + {"1.234E-16", "0.00000000000000000000", "0.00000000000000012340"}, + {"1.234E-16", "0.000000000000000000", "0.000000000000000123"}, + {"1.234E-16", "0.000000000000000000%", "0.000000000000012340%"}, + {"1.234E-16", "0.000000000000000000%%%%", "0.000000000000012340%"}, + // Unsupported number format + {"37947.7500001", "0.00000000E+000", "37947.7500001"}, + // Invalid number format + {"123", "x0.00s", "123"}, + {"-123", "x0.00s", "-123"}, + {"-1234.5678", ";E+;", "-1234.5678"}, + {"1234.5678", "E+;", "1234.5678"}, + {"1234.5678", "00000.00###s", "1234.5678"}, + {"-1234.5678", "00000.00###;s;", "-1234.5678"}, } { result := format(item[0], item[1], false, CellTypeNumber) assert.Equal(t, item[2], result, item) diff --git a/rows_test.go b/rows_test.go index 48a2735e57..4a91ab9945 100644 --- a/rows_test.go +++ b/rows_test.go @@ -1114,7 +1114,7 @@ func TestNumberFormats(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", cell, value)) result, err := f.GetCellValue("Sheet1", cell) assert.NoError(t, err) - assert.Equal(t, expected, result) + assert.Equal(t, expected, result, cell) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx"))) } diff --git a/styles.go b/styles.go index db7b5609fc..cb1210e165 100644 --- a/styles.go +++ b/styles.go @@ -34,7 +34,7 @@ var builtInNumFmt = map[int]string{ 4: "#,##0.00", 9: "0%", 10: "0.00%", - 11: "0.00e+00", + 11: "0.00E+00", 12: "# ?/?", 13: "# ??/??", 14: "mm-dd-yy", @@ -48,8 +48,8 @@ var builtInNumFmt = map[int]string{ 22: "m/d/yy hh:mm", 37: "#,##0 ;(#,##0)", 38: "#,##0 ;[red](#,##0)", - 39: "#,##0.00;(#,##0.00)", - 40: "#,##0.00;[red](#,##0.00)", + 39: "#,##0.00 ;(#,##0.00)", + 40: "#,##0.00 ;[red](#,##0.00)", 41: `_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)`, 42: `_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)`, 43: `_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)`, @@ -57,7 +57,7 @@ var builtInNumFmt = map[int]string{ 45: "mm:ss", 46: "[h]:mm:ss", 47: "mmss.0", - 48: "##0.0e+0", + 48: "##0.0E+0", 49: "@", } @@ -751,43 +751,6 @@ var currencyNumFmt = map[int]string{ 634: "[$ZWR]\\ #,##0.00", } -// builtInNumFmtFunc defined the format conversion functions map. Partial format -// code doesn't support currently and will return original string. -var builtInNumFmtFunc = map[int]func(v, format string, date1904 bool, cellType CellType) string{ - 0: format, - 1: formatToInt, - 2: formatToFloat, - 3: formatToIntSeparator, - 4: formatToFloat, - 9: formatToC, - 10: formatToD, - 11: formatToE, - 12: format, // Doesn't support currently - 13: format, // Doesn't support currently - 14: format, - 15: format, - 16: format, - 17: format, - 18: format, - 19: format, - 20: format, - 21: format, - 22: format, - 37: formatToA, - 38: formatToA, - 39: formatToB, - 40: formatToB, - 41: format, // Doesn't support currently - 42: format, // Doesn't support currently - 43: format, // Doesn't support currently - 44: format, // Doesn't support currently - 45: format, - 46: format, - 47: format, - 48: formatToE, - 49: format, -} - // validType defined the list of valid validation types. var validType = map[string]string{ "cell": "cellIs", @@ -869,172 +832,6 @@ var operatorType = map[string]string{ "greaterThanOrEqual": "greater than or equal to", } -// printCommaSep format number with thousands separator. -func printCommaSep(text string) string { - var ( - target strings.Builder - subStr = strings.Split(text, ".") - length = len(subStr[0]) - ) - for i := 0; i < length; i++ { - if i > 0 && (length-i)%3 == 0 { - target.WriteString(",") - } - target.WriteString(string(text[i])) - } - if len(subStr) == 2 { - target.WriteString(".") - target.WriteString(subStr[1]) - } - return target.String() -} - -// formatToInt provides a function to convert original string to integer -// format as string type by given built-in number formats code and cell -// string. -func formatToInt(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - return strconv.FormatFloat(math.Round(f), 'f', -1, 64) -} - -// formatToFloat provides a function to convert original string to float -// format as string type by given built-in number formats code and cell -// string. -func formatToFloat(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - source := strconv.FormatFloat(f, 'f', -1, 64) - if !strings.Contains(source, ".") { - return source + ".00" - } - return fmt.Sprintf("%.2f", f) -} - -// formatToIntSeparator provides a function to convert original string to -// integer format as string type by given built-in number formats code and cell -// string. -func formatToIntSeparator(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - return printCommaSep(strconv.FormatFloat(math.Round(f), 'f', -1, 64)) -} - -// formatToA provides a function to convert original string to special format -// as string type by given built-in number formats code and cell string. -func formatToA(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - var target strings.Builder - if f < 0 { - target.WriteString("(") - } - target.WriteString(printCommaSep(strconv.FormatFloat(math.Abs(math.Round(f)), 'f', -1, 64))) - if f < 0 { - target.WriteString(")") - } else { - target.WriteString(" ") - } - return target.String() -} - -// formatToB provides a function to convert original string to special format -// as string type by given built-in number formats code and cell string. -func formatToB(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - var target strings.Builder - if f < 0 { - target.WriteString("(") - } - source := strconv.FormatFloat(math.Abs(f), 'f', -1, 64) - var text string - if !strings.Contains(source, ".") { - text = printCommaSep(source + ".00") - } else { - text = printCommaSep(fmt.Sprintf("%.2f", math.Abs(f))) - } - target.WriteString(text) - if f < 0 { - target.WriteString(")") - } else { - target.WriteString(" ") - } - return target.String() -} - -// formatToC provides a function to convert original string to special format -// as string type by given built-in number formats code and cell string. -func formatToC(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - source := strconv.FormatFloat(f, 'f', -1, 64) - if !strings.Contains(source, ".") { - return source + "00%" - } - return fmt.Sprintf("%.f%%", f*100) -} - -// formatToD provides a function to convert original string to special format -// as string type by given built-in number formats code and cell string. -func formatToD(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - source := strconv.FormatFloat(f, 'f', -1, 64) - if !strings.Contains(source, ".") { - return source + "00.00%" - } - return fmt.Sprintf("%.2f%%", f*100) -} - -// formatToE provides a function to convert original string to special format -// as string type by given built-in number formats code and cell string. -func formatToE(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - return fmt.Sprintf("%.2E", f) -} - // stylesReader provides a function to get the pointer to the structure after // deserialization of xl/styles.xml. func (f *File) stylesReader() (*xlsxStyleSheet, error) { From bbdb83abf0a182f7203d434d44dd04a761648d73 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 4 May 2023 02:52:26 +0000 Subject: [PATCH 191/213] This closes #660, supports currency string, and switches argument for the number format code - Support round millisecond for the date time - Update built-in number formats mapping - Update unit tests - Upgrade dependencies package --- col_test.go | 4 +- excelize_test.go | 6 +- go.mod | 2 +- go.sum | 4 +- numfmt.go | 152 +++++++++++++++++++++++++++++++++-------------- numfmt_test.go | 26 +++++++- rows_test.go | 4 +- styles.go | 6 +- 8 files changed, 146 insertions(+), 58 deletions(-) diff --git a/col_test.go b/col_test.go index 0e686a958d..335bee0685 100644 --- a/col_test.go +++ b/col_test.go @@ -407,7 +407,7 @@ func TestInsertCols(t *testing.T) { f := NewFile() sheet1 := f.GetSheetName(0) - fillCells(f, sheet1, 10, 10) + assert.NoError(t, fillCells(f, sheet1, 10, 10)) assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.NoError(t, f.MergeCell(sheet1, "A1", "C3")) @@ -430,7 +430,7 @@ func TestRemoveCol(t *testing.T) { f := NewFile() sheet1 := f.GetSheetName(0) - fillCells(f, sheet1, 10, 15) + assert.NoError(t, fillCells(f, sheet1, 10, 15)) assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.NoError(t, f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External")) diff --git a/excelize_test.go b/excelize_test.go index 59ce3dfc42..6116b42533 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -750,10 +750,10 @@ func TestSetCellStyleNumberFormat(t *testing.T) { idxTbl := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49} value := []string{"37947.7500001", "-37947.7500001", "0.007", "2.1", "String"} expected := [][]string{ - {"37947.7500001", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 pm", "6:00:00 pm", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "0000.0", "37947.7500001", "37947.7500001"}, + {"37947.7500001", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 PM", "6:00:00 PM", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "00:00.0", "37947.7500001", "37947.7500001"}, {"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37,947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001"}, - {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 am", "0:10:04 am", "00:10", "00:10:04", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:04", "0:10:04", "1004.0", "0.007", "0.007"}, - {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 am", "2:24:00 am", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "2400.0", "2.1", "2.1"}, + {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 AM", "0:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:05", "0:10:05", "10:04.8", "0.007", "0.007"}, + {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 AM", "2:24:00 AM", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "24:00.0", "2.1", "2.1"}, {"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String"}, } diff --git a/go.mod b/go.mod index a266969a31..176c00a836 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.8.0 github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 - github.com/xuri/nfp v0.0.0-20230428090735-b50b0f0358f4 + github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 golang.org/x/crypto v0.8.0 golang.org/x/image v0.5.0 golang.org/x/net v0.9.0 diff --git a/go.sum b/go.sum index c57411bd7f..c5062b39c6 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E= github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/nfp v0.0.0-20230428090735-b50b0f0358f4 h1:YoU/1S7L25dvNepEir3Fg2aU9iGmDyE4gWKoEswWXts= -github.com/xuri/nfp v0.0.0-20230428090735-b50b0f0358f4/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 h1:xVwnvkzzi+OiwhIkWOXvh1skFI6bagk8OvGuazM80Rw= +github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= diff --git a/numfmt.go b/numfmt.go index 5f5f180fe4..283a4f88ce 100644 --- a/numfmt.go +++ b/numfmt.go @@ -36,9 +36,10 @@ type numberFormat struct { section []nfp.Section t time.Time sectionIdx int - date1904, isNumeric, hours, seconds bool + date1904, isNumeric, hours, seconds, useMillisecond bool number float64 ap, localCode, result, value, valueSectionType string + switchArgument, currencyString string fracHolder, fracPadding, intHolder, intPadding, expBaseLen int percent int useCommaSep, usePointer, usePositive, useScientificNotation bool @@ -47,6 +48,7 @@ type numberFormat struct { var ( // supportedTokenTypes list the supported number format token types currently. supportedTokenTypes = []string{ + nfp.TokenSubTypeCurrencyString, nfp.TokenSubTypeLanguageInfo, nfp.TokenTypeColor, nfp.TokenTypeCurrencyLanguage, @@ -58,23 +60,20 @@ var ( nfp.TokenTypeHashPlaceHolder, nfp.TokenTypeLiteral, nfp.TokenTypePercent, + nfp.TokenTypeSwitchArgument, nfp.TokenTypeTextPlaceHolder, nfp.TokenTypeThousandsSeparator, nfp.TokenTypeZeroPlaceHolder, } // supportedNumberTokenTypes list the supported number token types. supportedNumberTokenTypes = []string{ - nfp.TokenTypeColor, - nfp.TokenTypeDecimalPoint, + nfp.TokenTypeExponential, nfp.TokenTypeHashPlaceHolder, - nfp.TokenTypeLiteral, nfp.TokenTypePercent, - nfp.TokenTypeThousandsSeparator, nfp.TokenTypeZeroPlaceHolder, } // supportedDateTimeTokenTypes list the supported date and time token types. supportedDateTimeTokenTypes = []string{ - nfp.TokenTypeCurrencyLanguage, nfp.TokenTypeDateTimes, nfp.TokenTypeElapsedDateTimes, } @@ -357,6 +356,30 @@ var ( apFmtYi = "\ua3b8\ua111/\ua06f\ua2d2" // apFmtWelsh defined the AM/PM name in the Welsh. apFmtWelsh = "yb/yh" + // switchArgumentFunc defined the switch argument printer function + switchArgumentFunc = map[string]func(s string) string{ + "[DBNum1]": func(s string) string { + r := strings.NewReplacer( + "0", "\u25cb", "1", "\u4e00", "2", "\u4e8c", "3", "\u4e09", "4", "\u56db", + "5", "\u4e94", "6", "\u516d", "7", "\u4e03", "8", "\u516b", "9", "\u4e5d", + ) + return r.Replace(s) + }, + "[DBNum2]": func(s string) string { + r := strings.NewReplacer( + "0", "\u96f6", "1", "\u58f9", "2", "\u8d30", "3", "\u53c1", "4", "\u8086", + "5", "\u4f0d", "6", "\u9646", "7", "\u67d2", "8", "\u634c", "9", "\u7396", + ) + return r.Replace(s) + }, + "[DBNum3]": func(s string) string { + r := strings.NewReplacer( + "0", "\uff10", "1", "\uff11", "2", "\uff12", "3", "\uff13", "4", "\uff14", + "5", "\uff15", "6", "\uff16", "7", "\uff17", "8", "\uff18", "9", "\uff19", + ) + return r.Replace(s) + }, + } ) // prepareNumberic split the number into two before and after parts by a @@ -431,6 +454,9 @@ func (nf *numberFormat) getNumberFmtConf() { if token.TType == nfp.TokenTypeDecimalPoint { nf.usePointer = true } + if token.TType == nfp.TokenTypeSwitchArgument { + nf.switchArgument = token.TValue + } if token.TType == nfp.TokenTypeZeroPlaceHolder { if nf.usePointer { if nf.useScientificNotation { @@ -448,20 +474,34 @@ func (nf *numberFormat) getNumberFmtConf() { // printNumberLiteral apply literal tokens for the pre-formatted text. func (nf *numberFormat) printNumberLiteral(text string) string { var result string - var useZeroPlaceHolder bool + var useLiteral, useZeroPlaceHolder bool if nf.usePositive { result += "-" } - for _, token := range nf.section[nf.sectionIdx].Items { + for i, token := range nf.section[nf.sectionIdx].Items { + if token.TType == nfp.TokenTypeCurrencyLanguage { + if err := nf.currencyLanguageHandler(i, token); err != nil { + return nf.value + } + result += nf.currencyString + } if token.TType == nfp.TokenTypeLiteral { + if useZeroPlaceHolder { + useLiteral = true + } result += token.TValue } - if !useZeroPlaceHolder && token.TType == nfp.TokenTypeZeroPlaceHolder { - useZeroPlaceHolder = true - result += text + if token.TType == nfp.TokenTypeZeroPlaceHolder { + if useLiteral && useZeroPlaceHolder { + return nf.value + } + if !useZeroPlaceHolder { + useZeroPlaceHolder = true + result += text + } } } - return result + return nf.printSwitchArgument(result) } // printCommaSep format number with thousands separator. @@ -484,6 +524,17 @@ func printCommaSep(text string) string { return target.String() } +// printSwitchArgument format number with switch argument. +func (nf *numberFormat) printSwitchArgument(text string) string { + if nf.switchArgument == "" { + return text + } + if fn, ok := switchArgumentFunc[nf.switchArgument]; ok { + return fn(text) + } + return nf.value +} + // printBigNumber format number which precision great than 15 with fraction // zero padding and percentage symbol. func (nf *numberFormat) printBigNumber(decimal float64, fracLen int) string { @@ -561,14 +612,14 @@ func (nf *numberFormat) numberHandler() string { // dateTimeHandler handling data and time number format expression for a // positive numeric. -func (nf *numberFormat) dateTimeHandler() (result string) { +func (nf *numberFormat) dateTimeHandler() string { nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false for i, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { if err := nf.currencyLanguageHandler(i, token); err != nil { - result = nf.value - return + return nf.value } + nf.result += nf.currencyString } if token.TType == nfp.TokenTypeDateTimes { nf.dateTimesHandler(i, token) @@ -583,15 +634,18 @@ func (nf *numberFormat) dateTimeHandler() (result string) { if token.TType == nfp.TokenTypeDecimalPoint { nf.result += "." } + if token.TType == nfp.TokenTypeSwitchArgument { + nf.switchArgument = token.TValue + } if token.TType == nfp.TokenTypeZeroPlaceHolder { zeroHolderLen := len(token.TValue) if zeroHolderLen > 3 { zeroHolderLen = 3 } - nf.result += strings.Repeat("0", zeroHolderLen) + nf.result += fmt.Sprintf("%03d", nf.t.Nanosecond()/1e6)[:zeroHolderLen] } } - return nf.result + return nf.printSwitchArgument(nf.result) } // positiveHandler will be handling positive selection for a number format @@ -609,13 +663,26 @@ func (nf *numberFormat) positiveHandler() string { if fmtNum || nf.number < 0 { return nf.value } + var useDateTimeTokens bool + for _, token := range nf.section[nf.sectionIdx].Items { + if inStrSlice(supportedDateTimeTokenTypes, token.TType, false) != -1 { + if useDateTimeTokens && nf.useMillisecond { + return nf.value + } + useDateTimeTokens = true + } + if inStrSlice(supportedNumberTokenTypes, token.TType, false) != -1 { + if token.TType == nfp.TokenTypeZeroPlaceHolder { + nf.useMillisecond = true + continue + } + return nf.value + } + } return nf.dateTimeHandler() } } - if fmtNum { - return nf.numberHandler() - } - return nf.value + return nf.numberHandler() } // currencyLanguageHandler will be handling currency and language types tokens @@ -626,11 +693,16 @@ func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (err err err = ErrUnsupportedNumberFormat return } - if _, ok := supportedLanguageInfo[strings.ToUpper(part.Token.TValue)]; !ok { - err = ErrUnsupportedNumberFormat - return + if part.Token.TType == nfp.TokenSubTypeLanguageInfo { + if _, ok := supportedLanguageInfo[strings.ToUpper(part.Token.TValue)]; !ok { + err = ErrUnsupportedNumberFormat + return + } + nf.localCode = strings.ToUpper(part.Token.TValue) + } + if part.Token.TType == nfp.TokenSubTypeCurrencyString { + nf.currencyString = part.Token.TValue } - nf.localCode = strings.ToUpper(part.Token.TValue) } return } @@ -1039,17 +1111,17 @@ func (nf *numberFormat) minutesHandler(token nfp.Token) { // secondsHandler will be handling seconds in the date and times types tokens // for a number format expression. func (nf *numberFormat) secondsHandler(token nfp.Token) { - nf.seconds = strings.Contains(strings.ToUpper(token.TValue), "S") - if nf.seconds { - switch len(token.TValue) { - case 1: - nf.result += strconv.Itoa(nf.t.Second()) - return - default: - nf.result += fmt.Sprintf("%02d", nf.t.Second()) - return - } + if nf.seconds = strings.Contains(strings.ToUpper(token.TValue), "S"); !nf.seconds { + return + } + if !nf.useMillisecond { + nf.t = nf.t.Add(time.Duration(math.Round(float64(nf.t.Nanosecond())/1e9)) * time.Second) } + if len(token.TValue) == 1 { + nf.result += strconv.Itoa(nf.t.Second()) + return + } + nf.result += fmt.Sprintf("%02d", nf.t.Second()) } // elapsedDateTimesHandler will be handling elapsed date and times types tokens @@ -1114,23 +1186,15 @@ func (nf *numberFormat) secondsNext(i int) bool { // negativeHandler will be handling negative selection for a number format // expression. func (nf *numberFormat) negativeHandler() (result string) { - fmtNum := true for _, token := range nf.section[nf.sectionIdx].Items { if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { return nf.value } - if inStrSlice(supportedNumberTokenTypes, token.TType, true) != -1 { - continue - } if inStrSlice(supportedDateTimeTokenTypes, token.TType, true) != -1 { return nf.value } - fmtNum = false - } - if fmtNum { - return nf.numberHandler() } - return nf.value + return nf.numberHandler() } // zeroHandler will be handling zero selection for a number format expression. diff --git a/numfmt_test.go b/numfmt_test.go index bf5cbd280f..a8c9004799 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/xuri/nfp" ) func TestNumFmt(t *testing.T) { @@ -67,7 +68,7 @@ func TestNumFmt(t *testing.T) { {"43528", "[$-409]MM/DD/YYYY", "03/04/2019"}, {"43528", "[$-409]MM/DD/YYYY am/pm", "03/04/2019 AM"}, {"43528", "[$-111]MM/DD/YYYY", "43528"}, - {"43528", "[$US-409]MM/DD/YYYY", "43528"}, + {"43528", "[$US-409]MM/DD/YYYY", "US03/04/2019"}, {"43543.586539351854", "AM/PM h h:mm", "PM 14 2:04"}, {"text", "AM/PM h h:mm", "text"}, {"44562.189571759256", "[$-36]mmm dd yyyy h:mm AM/PM", "Jan. 01 2022 4:32 vm."}, @@ -1017,6 +1018,18 @@ func TestNumFmt(t *testing.T) { {"-1234.5678", "0.00;-0.00", "-1234.57"}, {"-1234.5678", "0.00%%", "-12345678.00%%"}, {"2.1", "mmss.0000", "2400.000"}, + {"0.007", "[h]:mm:ss.0", "0:10:04.8"}, + {"0.007", "[h]:mm:ss.00", "0:10:04.80"}, + {"0.007", "[h]:mm:ss.000", "0:10:04.800"}, + {"0.007", "[h]:mm:ss.0000", "0:10:04.800"}, + {"123", "[h]:mm,:ss.0", "2952:00,:00.0"}, + {"123", "yy-.dd", "00-.02"}, + {"123", "[DBNum1][$-804]yyyy\"年\"m\"月\";@", "\u4e00\u4e5d\u25cb\u25cb\u5e74\u4e94\u6708"}, + {"123", "[DBNum2][$-804]yyyy\"年\"m\"月\";@", "\u58f9\u7396\u96f6\u96f6\u5e74\u4f0d\u6708"}, + {"123", "[DBNum3][$-804]yyyy\"年\"m\"月\";@", "\uff11\uff19\uff10\uff10\u5e74\uff15\u6708"}, + {"1234567890", "[DBNum1][$-804]0.00", "\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u25cb.\u25cb\u25cb"}, + {"1234567890", "[DBNum2][$-804]0.00", "\u58f9\u8d30\u53c1\u8086\u4f0d\u9646\u67d2\u634c\u7396\u96f6.\u96f6\u96f6"}, + {"1234567890", "[DBNum3][$-804]0.00", "\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19\uff10.\uff10\uff10"}, {"1234.5678", "0.00###", "1234.5678"}, {"1234.5678", "00000.00###", "01234.5678"}, {"-1234.5678", "00000.00###;;", ""}, @@ -1029,14 +1042,23 @@ func TestNumFmt(t *testing.T) { {"1.234E-16", "0.000000000000000000", "0.000000000000000123"}, {"1.234E-16", "0.000000000000000000%", "0.000000000000012340%"}, {"1.234E-16", "0.000000000000000000%%%%", "0.000000000000012340%"}, + {"1234.5678", "[$$-409]#,##0.00", "$1,234.57"}, // Unsupported number format {"37947.7500001", "0.00000000E+000", "37947.7500001"}, + {"123", "[$kr.-46F]#,##0.00", "123"}, + {"123", "[$kr.-46F]MM/DD/YYYY", "123"}, + {"123", "[DBNum4][$-804]yyyy\"年\"m\"月\";@", "123"}, // Invalid number format {"123", "x0.00s", "123"}, + {"123", "[h]:m00m:ss", "123"}, + {"123", "yy-00dd", "123"}, + {"123", "yy-##dd", "123"}, + {"123", "xx[h]:mm,:ss.0xx", "xx2952:00,:00.0xx"}, {"-123", "x0.00s", "-123"}, {"-1234.5678", ";E+;", "-1234.5678"}, {"1234.5678", "E+;", "1234.5678"}, {"1234.5678", "00000.00###s", "1234.5678"}, + {"1234.5678", "0.0xxx00", "1234.5678"}, {"-1234.5678", "00000.00###;s;", "-1234.5678"}, } { result := format(item[0], item[1], false, CellTypeNumber) @@ -1055,4 +1077,6 @@ func TestNumFmt(t *testing.T) { assert.Equal(t, item[2], result, item) } } + nf := numberFormat{} + assert.Equal(t, ErrUnsupportedNumberFormat, nf.currencyLanguageHandler(0, nfp.Token{Parts: []nfp.Part{{}}})) } diff --git a/rows_test.go b/rows_test.go index 4a91ab9945..5a9dc824da 100644 --- a/rows_test.go +++ b/rows_test.go @@ -305,7 +305,7 @@ func TestRemoveRow(t *testing.T) { colCount = 10 rowCount = 10 ) - fillCells(f, sheet1, colCount, rowCount) + assert.NoError(t, fillCells(f, sheet1, colCount, rowCount)) assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) @@ -368,7 +368,7 @@ func TestInsertRows(t *testing.T) { colCount = 10 rowCount = 10 ) - fillCells(f, sheet1, colCount, rowCount) + assert.NoError(t, fillCells(f, sheet1, colCount, rowCount)) assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) diff --git a/styles.go b/styles.go index cb1210e165..bfef6a9657 100644 --- a/styles.go +++ b/styles.go @@ -41,8 +41,8 @@ var builtInNumFmt = map[int]string{ 15: "d-mmm-yy", 16: "d-mmm", 17: "mmm-yy", - 18: "h:mm am/pm", - 19: "h:mm:ss am/pm", + 18: "h:mm AM/PM", + 19: "h:mm:ss AM/PM", 20: "hh:mm", 21: "hh:mm:ss", 22: "m/d/yy hh:mm", @@ -56,7 +56,7 @@ var builtInNumFmt = map[int]string{ 44: `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`, 45: "mm:ss", 46: "[h]:mm:ss", - 47: "mmss.0", + 47: "mm:ss.0", 48: "##0.0E+0", 49: "@", } From dfdd97c0a7770cb83501b717d9084b634314de40 Mon Sep 17 00:00:00 2001 From: fudali Date: Sat, 6 May 2023 20:34:18 +0800 Subject: [PATCH 192/213] This closes #1199, support apply number format by system date and time options - Add new options `ShortDateFmtCode`, `LongDateFmtCode` and `LongTimeFmtCode` - Update unit tests --- cell.go | 13 +++++++++++-- cell_test.go | 4 ++-- excelize.go | 15 +++++++++++++++ file.go | 3 ++- numfmt.go | 33 +++++++++++++++++++++++---------- numfmt_test.go | 23 ++++++++++++++++++++--- rows_test.go | 9 +++++++++ 7 files changed, 82 insertions(+), 18 deletions(-) diff --git a/cell.go b/cell.go index 2ab05dc3ca..064eba4933 100644 --- a/cell.go +++ b/cell.go @@ -1336,6 +1336,15 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c return "", nil } +// applyBuiltInNumFmt provides a function to returns a value after formatted +// with built-in number format code, or specified sort date format code. +func (f *File) applyBuiltInNumFmt(c *xlsxC, fmtCode string, numFmtID int, date1904 bool, cellType CellType) string { + if numFmtID == 14 && f.options != nil && f.options.ShortDateFmtCode != "" { + fmtCode = f.options.ShortDateFmtCode + } + return format(c.V, fmtCode, date1904, cellType, f.options) +} + // formattedValue provides a function to returns a value after formatted. If // it is possible to apply a format to the cell value, it will do so, if not // then an error will be returned, along with the raw value of the cell. @@ -1366,14 +1375,14 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er date1904 = wb.WorkbookPr.Date1904 } if fmtCode, ok := builtInNumFmt[numFmtID]; ok { - return format(c.V, fmtCode, date1904, cellType), err + return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err } if styleSheet.NumFmts == nil { return c.V, err } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { - return format(c.V, xlsxFmt.FormatCode, date1904, cellType), err + return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options), err } } return c.V, err diff --git a/cell_test.go b/cell_test.go index ec7e5a32f0..7b53d86d19 100644 --- a/cell_test.go +++ b/cell_test.go @@ -873,7 +873,7 @@ func TestFormattedValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "311", result) - assert.Equal(t, "0_0", format("0_0", "", false, CellTypeNumber)) + assert.Equal(t, "0_0", format("0_0", "", false, CellTypeNumber, nil)) // Test format value with unsupported charset workbook f.WorkBook = nil @@ -887,7 +887,7 @@ func TestFormattedValue(t *testing.T) { _, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") - assert.Equal(t, "text", format("text", "0", false, CellTypeNumber)) + assert.Equal(t, "text", format("text", "0", false, CellTypeNumber, nil)) } func TestFormattedValueNilXfs(t *testing.T) { diff --git a/excelize.go b/excelize.go index 6c7ce2a181..7d84e57996 100644 --- a/excelize.go +++ b/excelize.go @@ -79,12 +79,27 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e // temporary directory when the file size is over this value, this value // should be less than or equal to UnzipSizeLimit, the default value is // 16MB. +// +// ShortDateFmtCode specifies the short date number format code. In the +// spreadsheet applications, date formats display date and time serial numbers +// as date values. Date formats that begin with an asterisk (*) respond to +// changes in regional date and time settings that are specified for the +// operating system. Formats without an asterisk are not affected by operating +// system settings. The ShortDateFmtCode used for specifies apply date formats +// that begin with an asterisk. +// +// LongDateFmtCode specifies the long date number format code. +// +// LongTimeFmtCode specifies the long time number format code. type Options struct { MaxCalcIterations uint Password string RawCellValue bool UnzipSizeLimit int64 UnzipXMLSizeLimit int64 + ShortDateFmtCode string + LongDateFmtCode string + LongTimeFmtCode string } // OpenFile take the name of an spreadsheet file and returns a populated diff --git a/file.go b/file.go index 416c934332..19333ea350 100644 --- a/file.go +++ b/file.go @@ -26,7 +26,7 @@ import ( // For example: // // f := NewFile() -func NewFile() *File { +func NewFile(opts ...Options) *File { f := newFile() f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels)) f.Pkg.Store(defaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp)) @@ -49,6 +49,7 @@ func NewFile() *File { ws, _ := f.workSheetReader("Sheet1") f.Sheet.Store("xl/worksheets/sheet1.xml", ws) f.Theme, _ = f.themeReader() + f.options = getOptions(opts...) return f } diff --git a/numfmt.go b/numfmt.go index 283a4f88ce..5e8155ef03 100644 --- a/numfmt.go +++ b/numfmt.go @@ -32,6 +32,7 @@ type languageInfo struct { // numberFormat directly maps the number format parser runtime required // fields. type numberFormat struct { + opts *Options cellType CellType section []nfp.Section t time.Time @@ -396,9 +397,9 @@ func (nf *numberFormat) prepareNumberic(value string) { // format provides a function to return a string parse by number format // expression. If the given number format is not supported, this will return // the original cell value. -func format(value, numFmt string, date1904 bool, cellType CellType) string { +func format(value, numFmt string, date1904 bool, cellType CellType, opts *Options) string { p := nfp.NumberFormatParser() - nf := numberFormat{section: p.Parse(numFmt), value: value, date1904: date1904, cellType: cellType} + nf := numberFormat{opts: opts, section: p.Parse(numFmt), value: value, date1904: date1904, cellType: cellType} nf.number, nf.valueSectionType = nf.getValueSectionType(value) nf.prepareNumberic(value) for i, section := range nf.section { @@ -480,7 +481,7 @@ func (nf *numberFormat) printNumberLiteral(text string) string { } for i, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { - if err := nf.currencyLanguageHandler(i, token); err != nil { + if err, changeNumFmtCode := nf.currencyLanguageHandler(i, token); err != nil || changeNumFmtCode { return nf.value } result += nf.currencyString @@ -616,7 +617,7 @@ func (nf *numberFormat) dateTimeHandler() string { nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false for i, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { - if err := nf.currencyLanguageHandler(i, token); err != nil { + if err, changeNumFmtCode := nf.currencyLanguageHandler(i, token); err != nil || changeNumFmtCode { return nf.value } nf.result += nf.currencyString @@ -687,16 +688,28 @@ func (nf *numberFormat) positiveHandler() string { // currencyLanguageHandler will be handling currency and language types tokens // for a number format expression. -func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (err error) { +func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (error, bool) { for _, part := range token.Parts { if inStrSlice(supportedTokenTypes, part.Token.TType, true) == -1 { - err = ErrUnsupportedNumberFormat - return + return ErrUnsupportedNumberFormat, false } if part.Token.TType == nfp.TokenSubTypeLanguageInfo { + if strings.EqualFold(part.Token.TValue, "F800") { // [$-x-sysdate] + if nf.opts != nil && nf.opts.LongDateFmtCode != "" { + nf.value = format(nf.value, nf.opts.LongDateFmtCode, nf.date1904, nf.cellType, nf.opts) + return nil, true + } + part.Token.TValue = "409" + } + if strings.EqualFold(part.Token.TValue, "F400") { // [$-x-systime] + if nf.opts != nil && nf.opts.LongTimeFmtCode != "" { + nf.value = format(nf.value, nf.opts.LongTimeFmtCode, nf.date1904, nf.cellType, nf.opts) + return nil, true + } + part.Token.TValue = "409" + } if _, ok := supportedLanguageInfo[strings.ToUpper(part.Token.TValue)]; !ok { - err = ErrUnsupportedNumberFormat - return + return ErrUnsupportedNumberFormat, false } nf.localCode = strings.ToUpper(part.Token.TValue) } @@ -704,7 +717,7 @@ func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (err err nf.currencyString = part.Token.TValue } } - return + return nil, false } // localAmPm return AM/PM name by supported language ID. diff --git a/numfmt_test.go b/numfmt_test.go index a8c9004799..21a657f697 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -995,6 +995,8 @@ func TestNumFmt(t *testing.T) { {"44835.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 AM"}, {"44866.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 AM"}, {"44896.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "D 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-F800]dddd, mmmm dd, yyyy", "Tuesday, March 19, 2019"}, + {"43543.503206018519", "[$-F400]h:mm:ss AM/PM", "12:04:37 PM"}, {"text_", "General", "text_"}, {"text_", "\"=====\"@@@\"--\"@\"----\"", "=====text_text_text_--text_----"}, {"0.0450685976001E+21", "0_);[Red]\\(0\\)", "45068597600100000000"}, @@ -1061,9 +1063,22 @@ func TestNumFmt(t *testing.T) { {"1234.5678", "0.0xxx00", "1234.5678"}, {"-1234.5678", "00000.00###;s;", "-1234.5678"}, } { - result := format(item[0], item[1], false, CellTypeNumber) + result := format(item[0], item[1], false, CellTypeNumber, nil) assert.Equal(t, item[2], result, item) } + // Test format number with specified date and time format code + for _, item := range [][]string{ + {"43543.503206018519", "[$-F800]dddd, mmmm dd, yyyy", "2019年3月19日"}, + {"43543.503206018519", "[$-F400]h:mm:ss AM/PM", "12:04:37"}, + } { + result := format(item[0], item[1], false, CellTypeNumber, &Options{ + ShortDateFmtCode: "yyyy/m/d", + LongDateFmtCode: "yyyy\"年\"M\"月\"d\"日\"", + LongTimeFmtCode: "H:mm:ss", + }) + assert.Equal(t, item[2], result, item) + } + // Test format number with string data type cell value for _, cellType := range []CellType{CellTypeSharedString, CellTypeInlineString} { for _, item := range [][]string{ {"1234.5678", "General", "1234.5678"}, @@ -1073,10 +1088,12 @@ func TestNumFmt(t *testing.T) { {"1234.5678", "0_);[Red]\\(0\\)", "1234.5678"}, {"1234.5678", "\"text\"@", "text1234.5678"}, } { - result := format(item[0], item[1], false, cellType) + result := format(item[0], item[1], false, cellType, nil) assert.Equal(t, item[2], result, item) } } nf := numberFormat{} - assert.Equal(t, ErrUnsupportedNumberFormat, nf.currencyLanguageHandler(0, nfp.Token{Parts: []nfp.Part{{}}})) + err, changeNumFmtCode := nf.currencyLanguageHandler(0, nfp.Token{Parts: []nfp.Part{{}}}) + assert.Equal(t, ErrUnsupportedNumberFormat, err) + assert.False(t, changeNumFmtCode) } diff --git a/rows_test.go b/rows_test.go index 5a9dc824da..f836cc053d 100644 --- a/rows_test.go +++ b/rows_test.go @@ -1117,6 +1117,15 @@ func TestNumberFormats(t *testing.T) { assert.Equal(t, expected, result, cell) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx"))) + + f = NewFile(Options{ShortDateFmtCode: "yyyy/m/d"}) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 43543.503206018519)) + numFmt14, err := f.NewStyle(&Style{NumFmt: 14}) + assert.NoError(t, err) + assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", numFmt14)) + result, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "2019/3/19", result, "A1") } func BenchmarkRows(b *testing.B) { From 49234fb95ea115357757251905857c8c8f53eddd Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 11 May 2023 09:08:38 +0800 Subject: [PATCH 193/213] Ref #1199, this support applies partial built-in language number format code - Remove the `Lang` field in the `Style` data type - Rename field name `ShortDateFmtCode` to `ShortDatePattern` in the `Options` data type - Rename field name `LongDateFmtCode` to `LongDatePattern` in the `Options` data type - Rename field name `LongTimeFmtCode` to `LongTimePattern` in the `Options` data type - Apply built-in language number format code number when creating a new style - Checking and returning error if the date and time pattern was invalid - Add new `Options` field `CultureInfo` and new exported data type `CultureName` - Add new culture name types enumeration for country code - Update unit tests - Move built-in number format code and currency number format code definition source code - Remove the built-in language number format code mapping with Unicode values - Fix incorrect number formatted result for date and time with 12 hours at AM --- cell.go | 11 +- excelize.go | 22 +- excelize_test.go | 68 +++- numfmt.go | 721 +++++++++++++++++++++++++++++++++++++- numfmt_test.go | 6 +- rows_test.go | 2 +- styles.go | 888 +---------------------------------------------- styles_test.go | 6 +- xmlStyles.go | 1 - 9 files changed, 806 insertions(+), 919 deletions(-) diff --git a/cell.go b/cell.go index 064eba4933..2de8e85385 100644 --- a/cell.go +++ b/cell.go @@ -1336,15 +1336,6 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c return "", nil } -// applyBuiltInNumFmt provides a function to returns a value after formatted -// with built-in number format code, or specified sort date format code. -func (f *File) applyBuiltInNumFmt(c *xlsxC, fmtCode string, numFmtID int, date1904 bool, cellType CellType) string { - if numFmtID == 14 && f.options != nil && f.options.ShortDateFmtCode != "" { - fmtCode = f.options.ShortDateFmtCode - } - return format(c.V, fmtCode, date1904, cellType, f.options) -} - // formattedValue provides a function to returns a value after formatted. If // it is possible to apply a format to the cell value, it will do so, if not // then an error will be returned, along with the raw value of the cell. @@ -1374,7 +1365,7 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er if wb != nil && wb.WorkbookPr != nil { date1904 = wb.WorkbookPr.Date1904 } - if fmtCode, ok := builtInNumFmt[numFmtID]; ok { + if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok { return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err } if styleSheet.NumFmts == nil { diff --git a/excelize.go b/excelize.go index 7d84e57996..d677285644 100644 --- a/excelize.go +++ b/excelize.go @@ -60,7 +60,7 @@ type File struct { // the spreadsheet from non-UTF-8 encoding. type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) -// Options define the options for open and reading spreadsheet. +// Options define the options for o`pen and reading spreadsheet. // // MaxCalcIterations specifies the maximum iterations for iterative // calculation, the default value is 0. @@ -80,26 +80,30 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e // should be less than or equal to UnzipSizeLimit, the default value is // 16MB. // -// ShortDateFmtCode specifies the short date number format code. In the +// ShortDatePattern specifies the short date number format code. In the // spreadsheet applications, date formats display date and time serial numbers // as date values. Date formats that begin with an asterisk (*) respond to // changes in regional date and time settings that are specified for the // operating system. Formats without an asterisk are not affected by operating -// system settings. The ShortDateFmtCode used for specifies apply date formats +// system settings. The ShortDatePattern used for specifies apply date formats // that begin with an asterisk. // -// LongDateFmtCode specifies the long date number format code. +// LongDatePattern specifies the long date number format code. // -// LongTimeFmtCode specifies the long time number format code. +// LongTimePattern specifies the long time number format code. +// +// CultureInfo specifies the country code for applying built-in language number +// format code these effect by the system's local language settings. type Options struct { MaxCalcIterations uint Password string RawCellValue bool UnzipSizeLimit int64 UnzipXMLSizeLimit int64 - ShortDateFmtCode string - LongDateFmtCode string - LongTimeFmtCode string + ShortDatePattern string + LongDatePattern string + LongTimePattern string + CultureInfo CultureName } // OpenFile take the name of an spreadsheet file and returns a populated @@ -162,7 +166,7 @@ func (f *File) checkOpenReaderOptions() error { if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit { return ErrOptionsUnzipSizeLimit } - return nil + return f.checkDateTimePattern() } // OpenReader read data stream from io.Reader and return a populated diff --git a/excelize_test.go b/excelize_test.go index 6116b42533..cba9552282 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -752,7 +752,7 @@ func TestSetCellStyleNumberFormat(t *testing.T) { expected := [][]string{ {"37947.7500001", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 PM", "6:00:00 PM", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "00:00.0", "37947.7500001", "37947.7500001"}, {"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37,947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001"}, - {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 AM", "0:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:05", "0:10:05", "10:04.8", "0.007", "0.007"}, + {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "12:10 AM", "12:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:05", "0:10:05", "10:04.8", "0.007", "0.007"}, {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 AM", "2:24:00 AM", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "24:00.0", "2.1", "2.1"}, {"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String"}, } @@ -811,19 +811,19 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5)) assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5)) - _, err = f.NewStyle(&Style{NumFmt: 26, Lang: "zh-tw"}) + _, err = f.NewStyle(&Style{NumFmt: 26}) assert.NoError(t, err) style, err := f.NewStyle(&Style{NumFmt: 27}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) - style, err = f.NewStyle(&Style{NumFmt: 31, Lang: "ko-kr"}) + style, err = f.NewStyle(&Style{NumFmt: 31}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) - style, err = f.NewStyle(&Style{NumFmt: 71, Lang: "th-th"}) + style, err = f.NewStyle(&Style{NumFmt: 71}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) @@ -831,6 +831,41 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { }) } +func TestSetCellStyleLangNumberFormat(t *testing.T) { + rawCellValues := [][]string{{"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}} + for lang, expected := range map[CultureName][][]string{ + CultureNameUnknown: rawCellValues, + CultureNameEnUS: {{"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"45162"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}}, + CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0时00分"}, {"0时00分00秒"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}}, + } { + f, err := prepareTestBook5(Options{CultureInfo: lang}) + assert.NoError(t, err) + rows, err := f.GetRows("Sheet1") + assert.NoError(t, err) + assert.Equal(t, expected, rows) + assert.NoError(t, f.Close()) + } + // Test apply language number format code with date and time pattern + for lang, expected := range map[CultureName][][]string{ + CultureNameEnUS: {{"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"45162"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}}, + CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}}, + } { + f, err := prepareTestBook5(Options{CultureInfo: lang, ShortDatePattern: "yyyy-M-d", LongTimePattern: "hh:mm:ss"}) + assert.NoError(t, err) + rows, err := f.GetRows("Sheet1") + assert.NoError(t, err) + assert.Equal(t, expected, rows) + assert.NoError(t, f.Close()) + } + // Test open workbook with invalid date and time pattern options + _, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{LongDatePattern: "0.00"}) + assert.Equal(t, ErrUnsupportedNumberFormat, err) + _, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{LongTimePattern: "0.00"}) + assert.Equal(t, ErrUnsupportedNumberFormat, err) + _, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{ShortDatePattern: "0.00"}) + assert.Equal(t, ErrUnsupportedNumberFormat, err) +} + func TestSetCellStyleCustomNumberFormat(t *testing.T) { f := NewFile() assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5)) @@ -1612,6 +1647,31 @@ func prepareTestBook4() (*File, error) { return f, nil } +func prepareTestBook5(opts Options) (*File, error) { + f := NewFile(opts) + var rowNum int + for _, idxRange := range [][]int{{27, 36}, {50, 81}} { + for numFmtIdx := idxRange[0]; numFmtIdx <= idxRange[1]; numFmtIdx++ { + rowNum++ + styleID, err := f.NewStyle(&Style{NumFmt: numFmtIdx}) + if err != nil { + return f, err + } + cell, err := CoordinatesToCellName(1, rowNum) + if err != nil { + return f, err + } + if err := f.SetCellValue("Sheet1", cell, 45162); err != nil { + return f, err + } + if err := f.SetCellStyle("Sheet1", cell, cell, styleID); err != nil { + return f, err + } + } + } + return f, nil +} + func fillCells(f *File, sheet string, colCount, rowCount int) error { for col := 1; col <= colCount; col++ { for row := 1; row <= rowCount; row++ { diff --git a/numfmt.go b/numfmt.go index 5e8155ef03..cea63a1ad9 100644 --- a/numfmt.go +++ b/numfmt.go @@ -46,7 +46,639 @@ type numberFormat struct { useCommaSep, usePointer, usePositive, useScientificNotation bool } +// CultureName is the type of supported language country codes types for apply +// number format. +type CultureName byte + +// This section defines the currently supported country code types enumeration +// for apply number format. +const ( + CultureNameUnknown CultureName = iota + CultureNameEnUS + CultureNameZhCN +) + var ( + // Excel styles can reference number formats that are built-in, all of which + // have an id less than 164. Note that this number format code list is under + // English localization. + builtInNumFmt = map[int]string{ + 0: "general", + 1: "0", + 2: "0.00", + 3: "#,##0", + 4: "#,##0.00", + 9: "0%", + 10: "0.00%", + 11: "0.00E+00", + 12: "# ?/?", + 13: "# ??/??", + 14: "mm-dd-yy", + 15: "d-mmm-yy", + 16: "d-mmm", + 17: "mmm-yy", + 18: "h:mm AM/PM", + 19: "h:mm:ss AM/PM", + 20: "hh:mm", + 21: "hh:mm:ss", + 22: "m/d/yy hh:mm", + 37: "#,##0 ;(#,##0)", + 38: "#,##0 ;[red](#,##0)", + 39: "#,##0.00 ;(#,##0.00)", + 40: "#,##0.00 ;[red](#,##0.00)", + 41: `_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)`, + 42: `_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)`, + 43: `_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)`, + 44: `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`, + 45: "mm:ss", + 46: "[h]:mm:ss", + 47: "mm:ss.0", + 48: "##0.0E+0", + 49: "@", + } + // langNumFmt defined number format code provided for language glyphs where + // they occur in different language. + langNumFmt = map[string]map[int]string{ + "zh-tw": { + 27: "[$-404]e/m/d", + 28: `[$-404]e"年"m"月"d"日"`, + 29: `[$-404]e"年"m"月"d"日"`, + 30: "m/d/yy", + 31: `yyyy"年"m"月"d"日"`, + 32: `hh"時"mm"分"`, + 33: `hh"時"mm"分"ss"秒"`, + 34: `上午/下午 hh"時"mm"分"`, + 35: `上午/下午 hh"時"mm"分"ss"秒"`, + 36: "[$-404]e/m/d", + 50: "[$-404]e/m/d", + 51: `[$-404]e"年"m"月"d"日"`, + 52: `上午/下午 hh"時"mm"分"`, + 53: `上午/下午 hh"時"mm"分"ss"秒"`, + 54: `[$-404]e"年"m"月"d"日"`, + 55: `上午/下午 hh"時"mm"分"`, + 56: `上午/下午 hh"時"mm"分"ss"秒"`, + 57: "[$-404]e/m/d", + 58: `[$-404]e"年"m"月"d"日"`, + }, + "zh-cn": { + 27: `yyyy"年"m"月"`, + 28: `m"月"d"日"`, + 29: `m"月"d"日"`, + 30: "m/d/yy", + 31: `yyyy"年"m"月"d"日"`, + 32: `h"时"mm"分"`, + 33: `h"时"mm"分"ss"秒"`, + 34: `上午/下午 h"时"mm"分"`, + 35: `上午/下午 h"时"mm"分"ss"秒"`, + 36: `yyyy"年"m"月"`, + 50: `yyyy"年"m"月"`, + 51: `m"月"d"日"`, + 52: `yyyy"年"m"月"`, + 53: `m"月"d"日"`, + 54: `m"月"d"日"`, + 55: `上午/下午 h"时"mm"分"`, + 56: `上午/下午 h"时"mm"分"ss"秒"`, + 57: `yyyy"年"m"月"`, + 58: `m"月"d"日"`, + }, + "ja-jp": { + 27: "[$-411]ge.m.d", + 28: `[$-411]ggge"年"m"月"d"日"`, + 29: `[$-411]ggge"年"m"月"d"日"`, + 30: "m/d/yy", + 31: `yyyy"年"m"月"d"日"`, + 32: `h"時"mm"分"`, + 33: `h"時"mm"分"ss"秒"`, + 34: `yyyy"年"m"月"`, + 35: `m"月"d"日"`, + 36: "[$-411]ge.m.d", + 50: "[$-411]ge.m.d", + 51: `[$-411]ggge"年"m"月"d"日"`, + 52: `yyyy"年"m"月"`, + 53: `m"月"d"日"`, + 54: `[$-411]ggge"年"m"月"d"日"`, + 55: `yyyy"年"m"月"`, + 56: `m"月"d"日"`, + 57: "[$-411]ge.m.d", + 58: `[$-411]ggge"年"m"月"d"日"`, + }, + "ko-kr": { + 27: `yyyy"年" mm"月" dd"日"`, + 28: "mm-dd", + 29: "mm-dd", + 30: "mm-dd-yy", + 31: `yyyy"년" mm"월" dd"일"`, + 32: `h"시" mm"분"`, + 33: `h"시" mm"분" ss"초"`, + 34: `yyyy-mm-dd`, + 35: `yyyy-mm-dd`, + 36: `yyyy"年" mm"月" dd"日"`, + 50: `yyyy"年" mm"月" dd"日"`, + 51: "mm-dd", + 52: "yyyy-mm-dd", + 53: "yyyy-mm-dd", + 54: "mm-dd", + 55: "yyyy-mm-dd", + 56: "yyyy-mm-dd", + 57: `yyyy"年" mm"月" dd"日"`, + 58: "mm-dd", + }, + "th-th": { + 59: "t0", + 60: "t0.00", + 61: "t#,##0", + 62: "t#,##0.00", + 67: "t0%", + 68: "t0.00%", + 69: "t# ?/?", + 70: "t# ??/??", + 71: "ว/ด/ปปปป", + 72: "ว-ดดด-ปป", + 73: "ว-ดดด", + 74: "ดดด-ปป", + 75: "ช:นน", + 76: "ช:นน:ทท", + 77: "ว/ด/ปปปป ช:นน", + 78: "นน:ทท", + 79: "[ช]:นน:ทท", + 80: "นน:ทท.0", + 81: "d/m/bb", + }, + } + // currencyNumFmt defined the currency number format map. + currencyNumFmt = map[int]string{ + 164: `"¥"#,##0.00`, + 165: "[$$-409]#,##0.00", + 166: "[$$-45C]#,##0.00", + 167: "[$$-1004]#,##0.00", + 168: "[$$-404]#,##0.00", + 169: "[$$-C09]#,##0.00", + 170: "[$$-2809]#,##0.00", + 171: "[$$-1009]#,##0.00", + 172: "[$$-2009]#,##0.00", + 173: "[$$-1409]#,##0.00", + 174: "[$$-4809]#,##0.00", + 175: "[$$-2C09]#,##0.00", + 176: "[$$-2409]#,##0.00", + 177: "[$$-1000]#,##0.00", + 178: `#,##0.00\ [$$-C0C]`, + 179: "[$$-475]#,##0.00", + 180: "[$$-83E]#,##0.00", + 181: `[$$-86B]\ #,##0.00`, + 182: `[$$-340A]\ #,##0.00`, + 183: "[$$-240A]#,##0.00", + 184: `[$$-300A]\ #,##0.00`, + 185: "[$$-440A]#,##0.00", + 186: "[$$-80A]#,##0.00", + 187: "[$$-500A]#,##0.00", + 188: "[$$-540A]#,##0.00", + 189: `[$$-380A]\ #,##0.00`, + 190: "[$£-809]#,##0.00", + 191: "[$£-491]#,##0.00", + 192: "[$£-452]#,##0.00", + 193: "[$¥-804]#,##0.00", + 194: "[$¥-411]#,##0.00", + 195: "[$¥-478]#,##0.00", + 196: "[$¥-451]#,##0.00", + 197: "[$¥-480]#,##0.00", + 198: "#,##0.00\\ [$\u058F-42B]", + 199: "[$\u060B-463]#,##0.00", + 200: "[$\u060B-48C]#,##0.00", + 201: "[$\u09F3-845]\\ #,##0.00", + 202: "#,##0.00[$\u17DB-453]", + 203: "[$\u20A1-140A]#,##0.00", + 204: "[$\u20A6-468]\\ #,##0.00", + 205: "[$\u20A6-470]\\ #,##0.00", + 206: "[$\u20A9-412]#,##0.00", + 207: "[$\u20AA-40D]\\ #,##0.00", + 208: "#,##0.00\\ [$\u20AB-42A]", + 209: "#,##0.00\\ [$\u20AC-42D]", + 210: "#,##0.00\\ [$\u20AC-47E]", + 211: "#,##0.00\\ [$\u20AC-403]", + 212: "#,##0.00\\ [$\u20AC-483]", + 213: "[$\u20AC-813]\\ #,##0.00", + 214: "[$\u20AC-413]\\ #,##0.00", + 215: "[$\u20AC-1809]#,##0.00", + 216: "#,##0.00\\ [$\u20AC-425]", + 217: "[$\u20AC-2]\\ #,##0.00", + 218: "#,##0.00\\ [$\u20AC-1]", + 219: "#,##0.00\\ [$\u20AC-40B]", + 220: "#,##0.00\\ [$\u20AC-80C]", + 221: "#,##0.00\\ [$\u20AC-40C]", + 222: "#,##0.00\\ [$\u20AC-140C]", + 223: "#,##0.00\\ [$\u20AC-180C]", + 224: "[$\u20AC-200C]#,##0.00", + 225: "#,##0.00\\ [$\u20AC-456]", + 226: "#,##0.00\\ [$\u20AC-C07]", + 227: "#,##0.00\\ [$\u20AC-407]", + 228: "#,##0.00\\ [$\u20AC-1007]", + 229: "#,##0.00\\ [$\u20AC-408]", + 230: "#,##0.00\\ [$\u20AC-243B]", + 231: "[$\u20AC-83C]#,##0.00", + 232: "[$\u20AC-410]\\ #,##0.00", + 233: "[$\u20AC-476]#,##0.00", + 234: "#,##0.00\\ [$\u20AC-2C1A]", + 235: "[$\u20AC-426]\\ #,##0.00", + 236: "#,##0.00\\ [$\u20AC-427]", + 237: "#,##0.00\\ [$\u20AC-82E]", + 238: "#,##0.00\\ [$\u20AC-46E]", + 239: "[$\u20AC-43A]#,##0.00", + 240: "#,##0.00\\ [$\u20AC-C3B]", + 241: "#,##0.00\\ [$\u20AC-482]", + 242: "#,##0.00\\ [$\u20AC-816]", + 243: "#,##0.00\\ [$\u20AC-301A]", + 244: "#,##0.00\\ [$\u20AC-203B]", + 245: "#,##0.00\\ [$\u20AC-41B]", + 246: "#,##0.00\\ [$\u20AC-424]", + 247: "#,##0.00\\ [$\u20AC-C0A]", + 248: "#,##0.00\\ [$\u20AC-81D]", + 249: "#,##0.00\\ [$\u20AC-484]", + 250: "#,##0.00\\ [$\u20AC-42E]", + 251: "[$\u20AC-462]\\ #,##0.00", + 252: "#,##0.00\\ [$₭-454]", + 253: "#,##0.00\\ [$₮-450]", + 254: "[$\u20AE-C50]#,##0.00", + 255: "[$\u20B1-3409]#,##0.00", + 256: "[$\u20B1-464]#,##0.00", + 257: "#,##0.00[$\u20B4-422]", + 258: "[$\u20B8-43F]#,##0.00", + 259: "[$\u20B9-460]#,##0.00", + 260: "[$\u20B9-4009]\\ #,##0.00", + 261: "[$\u20B9-447]\\ #,##0.00", + 262: "[$\u20B9-439]\\ #,##0.00", + 263: "[$\u20B9-44B]\\ #,##0.00", + 264: "[$\u20B9-860]#,##0.00", + 265: "[$\u20B9-457]\\ #,##0.00", + 266: "[$\u20B9-458]#,##0.00", + 267: "[$\u20B9-44E]\\ #,##0.00", + 268: "[$\u20B9-861]#,##0.00", + 269: "[$\u20B9-448]\\ #,##0.00", + 270: "[$\u20B9-446]\\ #,##0.00", + 271: "[$\u20B9-44F]\\ #,##0.00", + 272: "[$\u20B9-459]#,##0.00", + 273: "[$\u20B9-449]\\ #,##0.00", + 274: "[$\u20B9-820]#,##0.00", + 275: "#,##0.00\\ [$\u20BA-41F]", + 276: "#,##0.00\\ [$\u20BC-42C]", + 277: "#,##0.00\\ [$\u20BC-82C]", + 278: "#,##0.00\\ [$\u20BD-419]", + 279: "#,##0.00[$\u20BD-485]", + 280: "#,##0.00\\ [$\u20BE-437]", + 281: "[$B/.-180A]\\ #,##0.00", + 282: "[$Br-472]#,##0.00", + 283: "[$Br-477]#,##0.00", + 284: "#,##0.00[$Br-473]", + 285: "[$Bs-46B]\\ #,##0.00", + 286: "[$Bs-400A]\\ #,##0.00", + 287: "[$Bs.-200A]\\ #,##0.00", + 288: "[$BWP-832]\\ #,##0.00", + 289: "[$C$-4C0A]#,##0.00", + 290: "[$CA$-85D]#,##0.00", + 291: "[$CA$-47C]#,##0.00", + 292: "[$CA$-45D]#,##0.00", + 293: "[$CFA-340C]#,##0.00", + 294: "[$CFA-280C]#,##0.00", + 295: "#,##0.00\\ [$CFA-867]", + 296: "#,##0.00\\ [$CFA-488]", + 297: "#,##0.00\\ [$CHF-100C]", + 298: "[$CHF-1407]\\ #,##0.00", + 299: "[$CHF-807]\\ #,##0.00", + 300: "[$CHF-810]\\ #,##0.00", + 301: "[$CHF-417]\\ #,##0.00", + 302: "[$CLP-47A]\\ #,##0.00", + 303: "[$CN¥-850]#,##0.00", + 304: "#,##0.00\\ [$DZD-85F]", + 305: "[$FCFA-2C0C]#,##0.00", + 306: "#,##0.00\\ [$Ft-40E]", + 307: "[$G-3C0C]#,##0.00", + 308: "[$Gs.-3C0A]\\ #,##0.00", + 309: "[$GTQ-486]#,##0.00", + 310: "[$HK$-C04]#,##0.00", + 311: "[$HK$-3C09]#,##0.00", + 312: "#,##0.00\\ [$HRK-41A]", + 313: "[$IDR-3809]#,##0.00", + 314: "[$IQD-492]#,##0.00", + 315: "#,##0.00\\ [$ISK-40F]", + 316: "[$K-455]#,##0.00", + 317: "#,##0.00\\ [$K\u010D-405]", + 318: "#,##0.00\\ [$KM-141A]", + 319: "#,##0.00\\ [$KM-101A]", + 320: "#,##0.00\\ [$KM-181A]", + 321: "[$kr-438]\\ #,##0.00", + 322: "[$kr-43B]\\ #,##0.00", + 323: "#,##0.00\\ [$kr-83B]", + 324: "[$kr-414]\\ #,##0.00", + 325: "[$kr-814]\\ #,##0.00", + 326: "#,##0.00\\ [$kr-41D]", + 327: "[$kr.-406]\\ #,##0.00", + 328: "[$kr.-46F]\\ #,##0.00", + 329: "[$Ksh-441]#,##0.00", + 330: "[$L-818]#,##0.00", + 331: "[$L-819]#,##0.00", + 332: "[$L-480A]\\ #,##0.00", + 333: "#,##0.00\\ [$Lek\u00EB-41C]", + 334: "[$MAD-45F]#,##0.00", + 335: "[$MAD-380C]#,##0.00", + 336: "#,##0.00\\ [$MAD-105F]", + 337: "[$MOP$-1404]#,##0.00", + 338: "#,##0.00\\ [$MVR-465]_-", + 339: "#,##0.00[$Nfk-873]", + 340: "[$NGN-466]#,##0.00", + 341: "[$NGN-467]#,##0.00", + 342: "[$NGN-469]#,##0.00", + 343: "[$NGN-471]#,##0.00", + 344: "[$NOK-103B]\\ #,##0.00", + 345: "[$NOK-183B]\\ #,##0.00", + 346: "[$NZ$-481]#,##0.00", + 347: "[$PKR-859]\\ #,##0.00", + 348: "[$PYG-474]#,##0.00", + 349: "[$Q-100A]#,##0.00", + 350: "[$R-436]\\ #,##0.00", + 351: "[$R-1C09]\\ #,##0.00", + 352: "[$R-435]\\ #,##0.00", + 353: "[$R$-416]\\ #,##0.00", + 354: "[$RD$-1C0A]#,##0.00", + 355: "#,##0.00\\ [$RF-487]", + 356: "[$RM-4409]#,##0.00", + 357: "[$RM-43E]#,##0.00", + 358: "#,##0.00\\ [$RON-418]", + 359: "[$Rp-421]#,##0.00", + 360: "[$Rs-420]#,##0.00_-", + 361: "[$Rs.-849]\\ #,##0.00", + 362: "#,##0.00\\ [$RSD-81A]", + 363: "#,##0.00\\ [$RSD-C1A]", + 364: "#,##0.00\\ [$RUB-46D]", + 365: "#,##0.00\\ [$RUB-444]", + 366: "[$S/.-C6B]\\ #,##0.00", + 367: "[$S/.-280A]\\ #,##0.00", + 368: "#,##0.00\\ [$SEK-143B]", + 369: "#,##0.00\\ [$SEK-1C3B]", + 370: "#,##0.00\\ [$so\u02BBm-443]", + 371: "#,##0.00\\ [$so\u02BBm-843]", + 372: "#,##0.00\\ [$SYP-45A]", + 373: "[$THB-41E]#,##0.00", + 374: "#,##0.00[$TMT-442]", + 375: "[$US$-3009]#,##0.00", + 376: "[$ZAR-46C]\\ #,##0.00", + 377: "[$ZAR-430]#,##0.00", + 378: "[$ZAR-431]#,##0.00", + 379: "[$ZAR-432]\\ #,##0.00", + 380: "[$ZAR-433]#,##0.00", + 381: "[$ZAR-434]\\ #,##0.00", + 382: "#,##0.00\\ [$z\u0142-415]", + 383: "#,##0.00\\ [$\u0434\u0435\u043D-42F]", + 384: "#,##0.00\\ [$КМ-201A]", + 385: "#,##0.00\\ [$КМ-1C1A]", + 386: "#,##0.00\\ [$\u043B\u0432.-402]", + 387: "#,##0.00\\ [$р.-423]", + 388: "#,##0.00\\ [$\u0441\u043E\u043C-440]", + 389: "#,##0.00\\ [$\u0441\u043E\u043C-428]", + 390: "[$\u062C.\u0645.-C01]\\ #,##0.00_-", + 391: "[$\u062F.\u0623.-2C01]\\ #,##0.00_-", + 392: "[$\u062F.\u0625.-3801]\\ #,##0.00_-", + 393: "[$\u062F.\u0628.-3C01]\\ #,##0.00_-", + 394: "[$\u062F.\u062A.-1C01]\\ #,##0.00_-", + 395: "[$\u062F.\u062C.-1401]\\ #,##0.00_-", + 396: "[$\u062F.\u0639.-801]\\ #,##0.00_-", + 397: "[$\u062F.\u0643.-3401]\\ #,##0.00_-", + 398: "[$\u062F.\u0644.-1001]#,##0.00_-", + 399: "[$\u062F.\u0645.-1801]\\ #,##0.00_-", + 400: "[$\u0631-846]\\ #,##0.00", + 401: "[$\u0631.\u0633.-401]\\ #,##0.00_-", + 402: "[$\u0631.\u0639.-2001]\\ #,##0.00_-", + 403: "[$\u0631.\u0642.-4001]\\ #,##0.00_-", + 404: "[$\u0631.\u064A.-2401]\\ #,##0.00_-", + 405: "[$\u0631\u06CC\u0627\u0644-429]#,##0.00_-", + 406: "[$\u0644.\u0633.-2801]\\ #,##0.00_-", + 407: "[$\u0644.\u0644.-3001]\\ #,##0.00_-", + 408: "[$\u1265\u122D-45E]#,##0.00", + 409: "[$\u0930\u0942-461]#,##0.00", + 410: "[$\u0DBB\u0DD4.-45B]\\ #,##0.00", + 411: "[$ADP]\\ #,##0.00", + 412: "[$AED]\\ #,##0.00", + 413: "[$AFA]\\ #,##0.00", + 414: "[$AFN]\\ #,##0.00", + 415: "[$ALL]\\ #,##0.00", + 416: "[$AMD]\\ #,##0.00", + 417: "[$ANG]\\ #,##0.00", + 418: "[$AOA]\\ #,##0.00", + 419: "[$ARS]\\ #,##0.00", + 420: "[$ATS]\\ #,##0.00", + 421: "[$AUD]\\ #,##0.00", + 422: "[$AWG]\\ #,##0.00", + 423: "[$AZM]\\ #,##0.00", + 424: "[$AZN]\\ #,##0.00", + 425: "[$BAM]\\ #,##0.00", + 426: "[$BBD]\\ #,##0.00", + 427: "[$BDT]\\ #,##0.00", + 428: "[$BEF]\\ #,##0.00", + 429: "[$BGL]\\ #,##0.00", + 430: "[$BGN]\\ #,##0.00", + 431: "[$BHD]\\ #,##0.00", + 432: "[$BIF]\\ #,##0.00", + 433: "[$BMD]\\ #,##0.00", + 434: "[$BND]\\ #,##0.00", + 435: "[$BOB]\\ #,##0.00", + 436: "[$BOV]\\ #,##0.00", + 437: "[$BRL]\\ #,##0.00", + 438: "[$BSD]\\ #,##0.00", + 439: "[$BTN]\\ #,##0.00", + 440: "[$BWP]\\ #,##0.00", + 441: "[$BYR]\\ #,##0.00", + 442: "[$BZD]\\ #,##0.00", + 443: "[$CAD]\\ #,##0.00", + 444: "[$CDF]\\ #,##0.00", + 445: "[$CHE]\\ #,##0.00", + 446: "[$CHF]\\ #,##0.00", + 447: "[$CHW]\\ #,##0.00", + 448: "[$CLF]\\ #,##0.00", + 449: "[$CLP]\\ #,##0.00", + 450: "[$CNY]\\ #,##0.00", + 451: "[$COP]\\ #,##0.00", + 452: "[$COU]\\ #,##0.00", + 453: "[$CRC]\\ #,##0.00", + 454: "[$CSD]\\ #,##0.00", + 455: "[$CUC]\\ #,##0.00", + 456: "[$CVE]\\ #,##0.00", + 457: "[$CYP]\\ #,##0.00", + 458: "[$CZK]\\ #,##0.00", + 459: "[$DEM]\\ #,##0.00", + 460: "[$DJF]\\ #,##0.00", + 461: "[$DKK]\\ #,##0.00", + 462: "[$DOP]\\ #,##0.00", + 463: "[$DZD]\\ #,##0.00", + 464: "[$ECS]\\ #,##0.00", + 465: "[$ECV]\\ #,##0.00", + 466: "[$EEK]\\ #,##0.00", + 467: "[$EGP]\\ #,##0.00", + 468: "[$ERN]\\ #,##0.00", + 469: "[$ESP]\\ #,##0.00", + 470: "[$ETB]\\ #,##0.00", + 471: "[$EUR]\\ #,##0.00", + 472: "[$FIM]\\ #,##0.00", + 473: "[$FJD]\\ #,##0.00", + 474: "[$FKP]\\ #,##0.00", + 475: "[$FRF]\\ #,##0.00", + 476: "[$GBP]\\ #,##0.00", + 477: "[$GEL]\\ #,##0.00", + 478: "[$GHC]\\ #,##0.00", + 479: "[$GHS]\\ #,##0.00", + 480: "[$GIP]\\ #,##0.00", + 481: "[$GMD]\\ #,##0.00", + 482: "[$GNF]\\ #,##0.00", + 483: "[$GRD]\\ #,##0.00", + 484: "[$GTQ]\\ #,##0.00", + 485: "[$GYD]\\ #,##0.00", + 486: "[$HKD]\\ #,##0.00", + 487: "[$HNL]\\ #,##0.00", + 488: "[$HRK]\\ #,##0.00", + 489: "[$HTG]\\ #,##0.00", + 490: "[$HUF]\\ #,##0.00", + 491: "[$IDR]\\ #,##0.00", + 492: "[$IEP]\\ #,##0.00", + 493: "[$ILS]\\ #,##0.00", + 494: "[$INR]\\ #,##0.00", + 495: "[$IQD]\\ #,##0.00", + 496: "[$IRR]\\ #,##0.00", + 497: "[$ISK]\\ #,##0.00", + 498: "[$ITL]\\ #,##0.00", + 499: "[$JMD]\\ #,##0.00", + 500: "[$JOD]\\ #,##0.00", + 501: "[$JPY]\\ #,##0.00", + 502: "[$KAF]\\ #,##0.00", + 503: "[$KES]\\ #,##0.00", + 504: "[$KGS]\\ #,##0.00", + 505: "[$KHR]\\ #,##0.00", + 506: "[$KMF]\\ #,##0.00", + 507: "[$KPW]\\ #,##0.00", + 508: "[$KRW]\\ #,##0.00", + 509: "[$KWD]\\ #,##0.00", + 510: "[$KYD]\\ #,##0.00", + 511: "[$KZT]\\ #,##0.00", + 512: "[$LAK]\\ #,##0.00", + 513: "[$LBP]\\ #,##0.00", + 514: "[$LKR]\\ #,##0.00", + 515: "[$LRD]\\ #,##0.00", + 516: "[$LSL]\\ #,##0.00", + 517: "[$LTL]\\ #,##0.00", + 518: "[$LUF]\\ #,##0.00", + 519: "[$LVL]\\ #,##0.00", + 520: "[$LYD]\\ #,##0.00", + 521: "[$MAD]\\ #,##0.00", + 522: "[$MDL]\\ #,##0.00", + 523: "[$MGA]\\ #,##0.00", + 524: "[$MGF]\\ #,##0.00", + 525: "[$MKD]\\ #,##0.00", + 526: "[$MMK]\\ #,##0.00", + 527: "[$MNT]\\ #,##0.00", + 528: "[$MOP]\\ #,##0.00", + 529: "[$MRO]\\ #,##0.00", + 530: "[$MTL]\\ #,##0.00", + 531: "[$MUR]\\ #,##0.00", + 532: "[$MVR]\\ #,##0.00", + 533: "[$MWK]\\ #,##0.00", + 534: "[$MXN]\\ #,##0.00", + 535: "[$MXV]\\ #,##0.00", + 536: "[$MYR]\\ #,##0.00", + 537: "[$MZM]\\ #,##0.00", + 538: "[$MZN]\\ #,##0.00", + 539: "[$NAD]\\ #,##0.00", + 540: "[$NGN]\\ #,##0.00", + 541: "[$NIO]\\ #,##0.00", + 542: "[$NLG]\\ #,##0.00", + 543: "[$NOK]\\ #,##0.00", + 544: "[$NPR]\\ #,##0.00", + 545: "[$NTD]\\ #,##0.00", + 546: "[$NZD]\\ #,##0.00", + 547: "[$OMR]\\ #,##0.00", + 548: "[$PAB]\\ #,##0.00", + 549: "[$PEN]\\ #,##0.00", + 550: "[$PGK]\\ #,##0.00", + 551: "[$PHP]\\ #,##0.00", + 552: "[$PKR]\\ #,##0.00", + 553: "[$PLN]\\ #,##0.00", + 554: "[$PTE]\\ #,##0.00", + 555: "[$PYG]\\ #,##0.00", + 556: "[$QAR]\\ #,##0.00", + 557: "[$ROL]\\ #,##0.00", + 558: "[$RON]\\ #,##0.00", + 559: "[$RSD]\\ #,##0.00", + 560: "[$RUB]\\ #,##0.00", + 561: "[$RUR]\\ #,##0.00", + 562: "[$RWF]\\ #,##0.00", + 563: "[$SAR]\\ #,##0.00", + 564: "[$SBD]\\ #,##0.00", + 565: "[$SCR]\\ #,##0.00", + 566: "[$SDD]\\ #,##0.00", + 567: "[$SDG]\\ #,##0.00", + 568: "[$SDP]\\ #,##0.00", + 569: "[$SEK]\\ #,##0.00", + 570: "[$SGD]\\ #,##0.00", + 571: "[$SHP]\\ #,##0.00", + 572: "[$SIT]\\ #,##0.00", + 573: "[$SKK]\\ #,##0.00", + 574: "[$SLL]\\ #,##0.00", + 575: "[$SOS]\\ #,##0.00", + 576: "[$SPL]\\ #,##0.00", + 577: "[$SRD]\\ #,##0.00", + 578: "[$SRG]\\ #,##0.00", + 579: "[$STD]\\ #,##0.00", + 580: "[$SVC]\\ #,##0.00", + 581: "[$SYP]\\ #,##0.00", + 582: "[$SZL]\\ #,##0.00", + 583: "[$THB]\\ #,##0.00", + 584: "[$TJR]\\ #,##0.00", + 585: "[$TJS]\\ #,##0.00", + 586: "[$TMM]\\ #,##0.00", + 587: "[$TMT]\\ #,##0.00", + 588: "[$TND]\\ #,##0.00", + 589: "[$TOP]\\ #,##0.00", + 590: "[$TRL]\\ #,##0.00", + 591: "[$TRY]\\ #,##0.00", + 592: "[$TTD]\\ #,##0.00", + 593: "[$TWD]\\ #,##0.00", + 594: "[$TZS]\\ #,##0.00", + 595: "[$UAH]\\ #,##0.00", + 596: "[$UGX]\\ #,##0.00", + 597: "[$USD]\\ #,##0.00", + 598: "[$USN]\\ #,##0.00", + 599: "[$USS]\\ #,##0.00", + 600: "[$UYI]\\ #,##0.00", + 601: "[$UYU]\\ #,##0.00", + 602: "[$UZS]\\ #,##0.00", + 603: "[$VEB]\\ #,##0.00", + 604: "[$VEF]\\ #,##0.00", + 605: "[$VND]\\ #,##0.00", + 606: "[$VUV]\\ #,##0.00", + 607: "[$WST]\\ #,##0.00", + 608: "[$XAF]\\ #,##0.00", + 609: "[$XAG]\\ #,##0.00", + 610: "[$XAU]\\ #,##0.00", + 611: "[$XB5]\\ #,##0.00", + 612: "[$XBA]\\ #,##0.00", + 613: "[$XBB]\\ #,##0.00", + 614: "[$XBC]\\ #,##0.00", + 615: "[$XBD]\\ #,##0.00", + 616: "[$XCD]\\ #,##0.00", + 617: "[$XDR]\\ #,##0.00", + 618: "[$XFO]\\ #,##0.00", + 619: "[$XFU]\\ #,##0.00", + 620: "[$XOF]\\ #,##0.00", + 621: "[$XPD]\\ #,##0.00", + 622: "[$XPF]\\ #,##0.00", + 623: "[$XPT]\\ #,##0.00", + 624: "[$XTS]\\ #,##0.00", + 625: "[$XXX]\\ #,##0.00", + 626: "[$YER]\\ #,##0.00", + 627: "[$YUM]\\ #,##0.00", + 628: "[$ZAR]\\ #,##0.00", + 629: "[$ZMK]\\ #,##0.00", + 630: "[$ZMW]\\ #,##0.00", + 631: "[$ZWD]\\ #,##0.00", + 632: "[$ZWL]\\ #,##0.00", + 633: "[$ZWN]\\ #,##0.00", + 634: "[$ZWR]\\ #,##0.00", + } // supportedTokenTypes list the supported number format token types currently. supportedTokenTypes = []string{ nfp.TokenSubTypeCurrencyString, @@ -383,6 +1015,78 @@ var ( } ) +// applyBuiltInNumFmt provides a function to returns a value after formatted +// with built-in number format code, or specified sort date format code. +func (f *File) applyBuiltInNumFmt(c *xlsxC, fmtCode string, numFmtID int, date1904 bool, cellType CellType) string { + if numFmtID == 14 && f.options != nil && f.options.ShortDatePattern != "" { + fmtCode = f.options.ShortDatePattern + } + return format(c.V, fmtCode, date1904, cellType, f.options) +} + +// langNumFmtFuncEnUS returns number format code by given date and time pattern +// for country code en-us. +func (f *File) langNumFmtFuncEnUS(numFmtID int) string { + shortDatePattern, longTimePattern := "M/d/yy", "h:mm:ss" + if f.options.ShortDatePattern != "" { + shortDatePattern = f.options.ShortDatePattern + } + if f.options.LongTimePattern != "" { + longTimePattern = f.options.LongTimePattern + } + if 32 <= numFmtID && numFmtID <= 35 { + return longTimePattern + } + if (27 <= numFmtID && numFmtID <= 31) || (50 <= numFmtID && numFmtID <= 58) { + return shortDatePattern + } + return "" +} + +// checkDateTimePattern check and validate date and time options field value. +func (f *File) checkDateTimePattern() error { + for _, pattern := range []string{f.options.LongDatePattern, f.options.LongTimePattern, f.options.ShortDatePattern} { + p := nfp.NumberFormatParser() + for _, section := range p.Parse(pattern) { + for _, token := range section.Items { + if inStrSlice(supportedNumberTokenTypes, token.TType, false) == -1 || inStrSlice(supportedNumberTokenTypes, token.TType, false) != -1 { + return ErrUnsupportedNumberFormat + } + } + } + } + return nil +} + +// langNumFmtFuncZhCN returns number format code by given date and time pattern +// for country code zh-cn. +func (f *File) langNumFmtFuncZhCN(numFmtID int) string { + if numFmtID == 30 && f.options.ShortDatePattern != "" { + return f.options.ShortDatePattern + } + if (32 <= numFmtID && numFmtID <= 33) && f.options.LongTimePattern != "" { + return f.options.LongTimePattern + } + return langNumFmt["zh-cn"][numFmtID] +} + +// getBuiltInNumFmtCode convert number format index to number format code with +// specified locale and language. +func (f *File) getBuiltInNumFmtCode(numFmtID int) (string, bool) { + if fmtCode, ok := builtInNumFmt[numFmtID]; ok { + return fmtCode, true + } + if (27 <= numFmtID && numFmtID <= 36) || (50 <= numFmtID && numFmtID <= 81) { + if f.options.CultureInfo == CultureNameEnUS { + return f.langNumFmtFuncEnUS(numFmtID), true + } + if f.options.CultureInfo == CultureNameZhCN { + return f.langNumFmtFuncZhCN(numFmtID), true + } + } + return "", false +} + // prepareNumberic split the number into two before and after parts by a // decimal point. func (nf *numberFormat) prepareNumberic(value string) { @@ -695,15 +1399,15 @@ func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (error, } if part.Token.TType == nfp.TokenSubTypeLanguageInfo { if strings.EqualFold(part.Token.TValue, "F800") { // [$-x-sysdate] - if nf.opts != nil && nf.opts.LongDateFmtCode != "" { - nf.value = format(nf.value, nf.opts.LongDateFmtCode, nf.date1904, nf.cellType, nf.opts) + if nf.opts != nil && nf.opts.LongDatePattern != "" { + nf.value = format(nf.value, nf.opts.LongDatePattern, nf.date1904, nf.cellType, nf.opts) return nil, true } part.Token.TValue = "409" } if strings.EqualFold(part.Token.TValue, "F400") { // [$-x-systime] - if nf.opts != nil && nf.opts.LongTimeFmtCode != "" { - nf.value = format(nf.value, nf.opts.LongTimeFmtCode, nf.date1904, nf.cellType, nf.opts) + if nf.opts != nil && nf.opts.LongTimePattern != "" { + nf.value = format(nf.value, nf.opts.LongTimePattern, nf.date1904, nf.cellType, nf.opts) return nil, true } part.Token.TValue = "409" @@ -1091,8 +1795,13 @@ func (nf *numberFormat) hoursHandler(i int, token nfp.Token) { h -= 12 } } - if nf.ap != "" && nf.hoursNext(i) == -1 && h > 12 { - h -= 12 + if nf.ap != "" { + if nf.hoursNext(i) == -1 && h > 12 { + h -= 12 + } + if h == 0 { + h = 12 + } } switch len(token.TValue) { case 1: diff --git a/numfmt_test.go b/numfmt_test.go index 21a657f697..c41fd9408d 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -1072,9 +1072,9 @@ func TestNumFmt(t *testing.T) { {"43543.503206018519", "[$-F400]h:mm:ss AM/PM", "12:04:37"}, } { result := format(item[0], item[1], false, CellTypeNumber, &Options{ - ShortDateFmtCode: "yyyy/m/d", - LongDateFmtCode: "yyyy\"年\"M\"月\"d\"日\"", - LongTimeFmtCode: "H:mm:ss", + ShortDatePattern: "yyyy/m/d", + LongDatePattern: "yyyy\"年\"M\"月\"d\"日\"", + LongTimePattern: "H:mm:ss", }) assert.Equal(t, item[2], result, item) } diff --git a/rows_test.go b/rows_test.go index f836cc053d..f94adbd037 100644 --- a/rows_test.go +++ b/rows_test.go @@ -1118,7 +1118,7 @@ func TestNumberFormats(t *testing.T) { } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx"))) - f = NewFile(Options{ShortDateFmtCode: "yyyy/m/d"}) + f = NewFile(Options{ShortDatePattern: "yyyy/m/d"}) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 43543.503206018519)) numFmt14, err := f.NewStyle(&Style{NumFmt: 14}) assert.NoError(t, err) diff --git a/styles.go b/styles.go index bfef6a9657..960aa89ee0 100644 --- a/styles.go +++ b/styles.go @@ -23,734 +23,6 @@ import ( "strings" ) -// Excel styles can reference number formats that are built-in, all of which -// have an id less than 164. Note that this number format code list is under -// English localization. -var builtInNumFmt = map[int]string{ - 0: "general", - 1: "0", - 2: "0.00", - 3: "#,##0", - 4: "#,##0.00", - 9: "0%", - 10: "0.00%", - 11: "0.00E+00", - 12: "# ?/?", - 13: "# ??/??", - 14: "mm-dd-yy", - 15: "d-mmm-yy", - 16: "d-mmm", - 17: "mmm-yy", - 18: "h:mm AM/PM", - 19: "h:mm:ss AM/PM", - 20: "hh:mm", - 21: "hh:mm:ss", - 22: "m/d/yy hh:mm", - 37: "#,##0 ;(#,##0)", - 38: "#,##0 ;[red](#,##0)", - 39: "#,##0.00 ;(#,##0.00)", - 40: "#,##0.00 ;[red](#,##0.00)", - 41: `_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)`, - 42: `_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)`, - 43: `_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)`, - 44: `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`, - 45: "mm:ss", - 46: "[h]:mm:ss", - 47: "mm:ss.0", - 48: "##0.0E+0", - 49: "@", -} - -// langNumFmt defined number format code (with unicode values provided for -// language glyphs where they occur) in different language. -var langNumFmt = map[string]map[int]string{ - "zh-tw": { - 27: "[$-404]e/m/d", - 28: `[$-404]e"年"m"月"d"日"`, - 29: `[$-404]e"年"m"月"d"日"`, - 30: "m/d/yy", - 31: `yyyy"年"m"月"d"日"`, - 32: `hh"時"mm"分"`, - 33: `hh"時"mm"分"ss"秒"`, - 34: `上午/下午 hh"時"mm"分"`, - 35: `上午/下午 hh"時"mm"分"ss"秒"`, - 36: "[$-404]e/m/d", - 50: "[$-404]e/m/d", - 51: `[$-404]e"年"m"月"d"日"`, - 52: `上午/下午 hh"時"mm"分"`, - 53: `上午/下午 hh"時"mm"分"ss"秒"`, - 54: `[$-404]e"年"m"月"d"日"`, - 55: `上午/下午 hh"時"mm"分"`, - 56: `上午/下午 hh"時"mm"分"ss"秒"`, - 57: "[$-404]e/m/d", - 58: `[$-404]e"年"m"月"d"日"`, - }, - "zh-cn": { - 27: `yyyy"年"m"月"`, - 28: `m"月"d"日"`, - 29: `m"月"d"日"`, - 30: "m-d-yy", - 31: `yyyy"年"m"月"d"日"`, - 32: `h"时"mm"分"`, - 33: `h"时"mm"分"ss"秒"`, - 34: `上午/下午 h"时"mm"分"`, - 35: `上午/下午 h"时"mm"分"ss"秒"`, - 36: `yyyy"年"m"月"`, - 50: `yyyy"年"m"月"`, - 51: `m"月"d"日"`, - 52: `yyyy"年"m"月"`, - 53: `m"月"d"日"`, - 54: `m"月"d"日"`, - 55: `上午/下午 h"时"mm"分"`, - 56: `上午/下午 h"时"mm"分"ss"秒"`, - 57: `yyyy"年"m"月"`, - 58: `m"月"d"日"`, - }, - "zh-tw_unicode": { - 27: "[$-404]e/m/d", - 28: `[$-404]e"5E74"m"6708"d"65E5"`, - 29: `[$-404]e"5E74"m"6708"d"65E5"`, - 30: "m/d/yy", - 31: `yyyy"5E74"m"6708"d"65E5"`, - 32: `hh"6642"mm"5206"`, - 33: `hh"6642"mm"5206"ss"79D2"`, - 34: `4E0A5348/4E0B5348hh"6642"mm"5206"`, - 35: `4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2"`, - 36: "[$-404]e/m/d", - 50: "[$-404]e/m/d", - 51: `[$-404]e"5E74"m"6708"d"65E5"`, - 52: `4E0A5348/4E0B5348hh"6642"mm"5206"`, - 53: `4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2"`, - 54: `[$-404]e"5E74"m"6708"d"65E5"`, - 55: `4E0A5348/4E0B5348hh"6642"mm"5206"`, - 56: `4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2"`, - 57: "[$-404]e/m/d", - 58: `[$-404]e"5E74"m"6708"d"65E5"`, - }, - "zh-cn_unicode": { - 27: `yyyy"5E74"m"6708"`, - 28: `m"6708"d"65E5"`, - 29: `m"6708"d"65E5"`, - 30: "m-d-yy", - 31: `yyyy"5E74"m"6708"d"65E5"`, - 32: `h"65F6"mm"5206"`, - 33: `h"65F6"mm"5206"ss"79D2"`, - 34: `4E0A5348/4E0B5348h"65F6"mm"5206"`, - 35: `4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2"`, - 36: `yyyy"5E74"m"6708"`, - 50: `yyyy"5E74"m"6708"`, - 51: `m"6708"d"65E5"`, - 52: `yyyy"5E74"m"6708"`, - 53: `m"6708"d"65E5"`, - 54: `m"6708"d"65E5"`, - 55: `4E0A5348/4E0B5348h"65F6"mm"5206"`, - 56: `4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2"`, - 57: `yyyy"5E74"m"6708"`, - 58: `m"6708"d"65E5"`, - }, - "ja-jp": { - 27: "[$-411]ge.m.d", - 28: `[$-411]ggge"年"m"月"d"日"`, - 29: `[$-411]ggge"年"m"月"d"日"`, - 30: "m/d/yy", - 31: `yyyy"年"m"月"d"日"`, - 32: `h"時"mm"分"`, - 33: `h"時"mm"分"ss"秒"`, - 34: `yyyy"年"m"月"`, - 35: `m"月"d"日"`, - 36: "[$-411]ge.m.d", - 50: "[$-411]ge.m.d", - 51: `[$-411]ggge"年"m"月"d"日"`, - 52: `yyyy"年"m"月"`, - 53: `m"月"d"日"`, - 54: `[$-411]ggge"年"m"月"d"日"`, - 55: `yyyy"年"m"月"`, - 56: `m"月"d"日"`, - 57: "[$-411]ge.m.d", - 58: `[$-411]ggge"年"m"月"d"日"`, - }, - "ko-kr": { - 27: `yyyy"年" mm"月" dd"日"`, - 28: "mm-dd", - 29: "mm-dd", - 30: "mm-dd-yy", - 31: `yyyy"년" mm"월" dd"일"`, - 32: `h"시" mm"분"`, - 33: `h"시" mm"분" ss"초"`, - 34: `yyyy-mm-dd`, - 35: `yyyy-mm-dd`, - 36: `yyyy"年" mm"月" dd"日"`, - 50: `yyyy"年" mm"月" dd"日"`, - 51: "mm-dd", - 52: "yyyy-mm-dd", - 53: "yyyy-mm-dd", - 54: "mm-dd", - 55: "yyyy-mm-dd", - 56: "yyyy-mm-dd", - 57: `yyyy"年" mm"月" dd"日"`, - 58: "mm-dd", - }, - "ja-jp_unicode": { - 27: "[$-411]ge.m.d", - 28: `[$-411]ggge"5E74"m"6708"d"65E5"`, - 29: `[$-411]ggge"5E74"m"6708"d"65E5"`, - 30: "m/d/yy", - 31: `yyyy"5E74"m"6708"d"65E5"`, - 32: `h"6642"mm"5206"`, - 33: `h"6642"mm"5206"ss"79D2"`, - 34: `yyyy"5E74"m"6708"`, - 35: `m"6708"d"65E5"`, - 36: "[$-411]ge.m.d", - 50: "[$-411]ge.m.d", - 51: `[$-411]ggge"5E74"m"6708"d"65E5"`, - 52: `yyyy"5E74"m"6708"`, - 53: `m"6708"d"65E5"`, - 54: `[$-411]ggge"5E74"m"6708"d"65E5"`, - 55: `yyyy"5E74"m"6708"`, - 56: `m"6708"d"65E5"`, - 57: "[$-411]ge.m.d", - 58: `[$-411]ggge"5E74"m"6708"d"65E5"`, - }, - "ko-kr_unicode": { - 27: `yyyy"5E74" mm"6708" dd"65E5"`, - 28: "mm-dd", - 29: "mm-dd", - 30: "mm-dd-yy", - 31: `yyyy"B144" mm"C6D4" dd"C77C"`, - 32: `h"C2DC" mm"BD84"`, - 33: `h"C2DC" mm"BD84" ss"CD08"`, - 34: "yyyy-mm-dd", - 35: "yyyy-mm-dd", - 36: `yyyy"5E74" mm"6708" dd"65E5"`, - 50: `yyyy"5E74" mm"6708" dd"65E5"`, - 51: "mm-dd", - 52: "yyyy-mm-dd", - 53: "yyyy-mm-dd", - 54: "mm-dd", - 55: "yyyy-mm-dd", - 56: "yyyy-mm-dd", - 57: `yyyy"5E74" mm"6708" dd"65E5"`, - 58: "mm-dd", - }, - "th-th": { - 59: "t0", - 60: "t0.00", - 61: "t#,##0", - 62: "t#,##0.00", - 67: "t0%", - 68: "t0.00%", - 69: "t# ?/?", - 70: "t# ??/??", - 71: "ว/ด/ปปปป", - 72: "ว-ดดด-ปป", - 73: "ว-ดดด", - 74: "ดดด-ปป", - 75: "ช:นน", - 76: "ช:นน:ทท", - 77: "ว/ด/ปปปป ช:นน", - 78: "นน:ทท", - 79: "[ช]:นน:ทท", - 80: "นน:ทท.0", - 81: "d/m/bb", - }, - "th-th_unicode": { - 59: "t0", - 60: "t0.00", - 61: "t#,##0", - 62: "t#,##0.00", - 67: "t0%", - 68: "t0.00%", - 69: "t# ?/?", - 70: "t# ??/??", - 71: "0E27/0E14/0E1B0E1B0E1B0E1B", - 72: "0E27-0E140E140E14-0E1B0E1B", - 73: "0E27-0E140E140E14", - 74: "0E140E140E14-0E1B0E1B", - 75: "0E0A:0E190E19", - 76: "0E0A:0E190E19:0E170E17", - 77: "0E27/0E14/0E1B0E1B0E1B0E1B 0E0A:0E190E19", - 78: "0E190E19:0E170E17", - 79: "[0E0A]:0E190E19:0E170E17", - 80: "0E190E19:0E170E17.0", - 81: "d/m/bb", - }, -} - -// currencyNumFmt defined the currency number format map. -var currencyNumFmt = map[int]string{ - 164: `"¥"#,##0.00`, - 165: "[$$-409]#,##0.00", - 166: "[$$-45C]#,##0.00", - 167: "[$$-1004]#,##0.00", - 168: "[$$-404]#,##0.00", - 169: "[$$-C09]#,##0.00", - 170: "[$$-2809]#,##0.00", - 171: "[$$-1009]#,##0.00", - 172: "[$$-2009]#,##0.00", - 173: "[$$-1409]#,##0.00", - 174: "[$$-4809]#,##0.00", - 175: "[$$-2C09]#,##0.00", - 176: "[$$-2409]#,##0.00", - 177: "[$$-1000]#,##0.00", - 178: `#,##0.00\ [$$-C0C]`, - 179: "[$$-475]#,##0.00", - 180: "[$$-83E]#,##0.00", - 181: `[$$-86B]\ #,##0.00`, - 182: `[$$-340A]\ #,##0.00`, - 183: "[$$-240A]#,##0.00", - 184: `[$$-300A]\ #,##0.00`, - 185: "[$$-440A]#,##0.00", - 186: "[$$-80A]#,##0.00", - 187: "[$$-500A]#,##0.00", - 188: "[$$-540A]#,##0.00", - 189: `[$$-380A]\ #,##0.00`, - 190: "[$£-809]#,##0.00", - 191: "[$£-491]#,##0.00", - 192: "[$£-452]#,##0.00", - 193: "[$¥-804]#,##0.00", - 194: "[$¥-411]#,##0.00", - 195: "[$¥-478]#,##0.00", - 196: "[$¥-451]#,##0.00", - 197: "[$¥-480]#,##0.00", - 198: "#,##0.00\\ [$\u058F-42B]", - 199: "[$\u060B-463]#,##0.00", - 200: "[$\u060B-48C]#,##0.00", - 201: "[$\u09F3-845]\\ #,##0.00", - 202: "#,##0.00[$\u17DB-453]", - 203: "[$\u20A1-140A]#,##0.00", - 204: "[$\u20A6-468]\\ #,##0.00", - 205: "[$\u20A6-470]\\ #,##0.00", - 206: "[$\u20A9-412]#,##0.00", - 207: "[$\u20AA-40D]\\ #,##0.00", - 208: "#,##0.00\\ [$\u20AB-42A]", - 209: "#,##0.00\\ [$\u20AC-42D]", - 210: "#,##0.00\\ [$\u20AC-47E]", - 211: "#,##0.00\\ [$\u20AC-403]", - 212: "#,##0.00\\ [$\u20AC-483]", - 213: "[$\u20AC-813]\\ #,##0.00", - 214: "[$\u20AC-413]\\ #,##0.00", - 215: "[$\u20AC-1809]#,##0.00", - 216: "#,##0.00\\ [$\u20AC-425]", - 217: "[$\u20AC-2]\\ #,##0.00", - 218: "#,##0.00\\ [$\u20AC-1]", - 219: "#,##0.00\\ [$\u20AC-40B]", - 220: "#,##0.00\\ [$\u20AC-80C]", - 221: "#,##0.00\\ [$\u20AC-40C]", - 222: "#,##0.00\\ [$\u20AC-140C]", - 223: "#,##0.00\\ [$\u20AC-180C]", - 224: "[$\u20AC-200C]#,##0.00", - 225: "#,##0.00\\ [$\u20AC-456]", - 226: "#,##0.00\\ [$\u20AC-C07]", - 227: "#,##0.00\\ [$\u20AC-407]", - 228: "#,##0.00\\ [$\u20AC-1007]", - 229: "#,##0.00\\ [$\u20AC-408]", - 230: "#,##0.00\\ [$\u20AC-243B]", - 231: "[$\u20AC-83C]#,##0.00", - 232: "[$\u20AC-410]\\ #,##0.00", - 233: "[$\u20AC-476]#,##0.00", - 234: "#,##0.00\\ [$\u20AC-2C1A]", - 235: "[$\u20AC-426]\\ #,##0.00", - 236: "#,##0.00\\ [$\u20AC-427]", - 237: "#,##0.00\\ [$\u20AC-82E]", - 238: "#,##0.00\\ [$\u20AC-46E]", - 239: "[$\u20AC-43A]#,##0.00", - 240: "#,##0.00\\ [$\u20AC-C3B]", - 241: "#,##0.00\\ [$\u20AC-482]", - 242: "#,##0.00\\ [$\u20AC-816]", - 243: "#,##0.00\\ [$\u20AC-301A]", - 244: "#,##0.00\\ [$\u20AC-203B]", - 245: "#,##0.00\\ [$\u20AC-41B]", - 246: "#,##0.00\\ [$\u20AC-424]", - 247: "#,##0.00\\ [$\u20AC-C0A]", - 248: "#,##0.00\\ [$\u20AC-81D]", - 249: "#,##0.00\\ [$\u20AC-484]", - 250: "#,##0.00\\ [$\u20AC-42E]", - 251: "[$\u20AC-462]\\ #,##0.00", - 252: "#,##0.00\\ [$₭-454]", - 253: "#,##0.00\\ [$₮-450]", - 254: "[$\u20AE-C50]#,##0.00", - 255: "[$\u20B1-3409]#,##0.00", - 256: "[$\u20B1-464]#,##0.00", - 257: "#,##0.00[$\u20B4-422]", - 258: "[$\u20B8-43F]#,##0.00", - 259: "[$\u20B9-460]#,##0.00", - 260: "[$\u20B9-4009]\\ #,##0.00", - 261: "[$\u20B9-447]\\ #,##0.00", - 262: "[$\u20B9-439]\\ #,##0.00", - 263: "[$\u20B9-44B]\\ #,##0.00", - 264: "[$\u20B9-860]#,##0.00", - 265: "[$\u20B9-457]\\ #,##0.00", - 266: "[$\u20B9-458]#,##0.00", - 267: "[$\u20B9-44E]\\ #,##0.00", - 268: "[$\u20B9-861]#,##0.00", - 269: "[$\u20B9-448]\\ #,##0.00", - 270: "[$\u20B9-446]\\ #,##0.00", - 271: "[$\u20B9-44F]\\ #,##0.00", - 272: "[$\u20B9-459]#,##0.00", - 273: "[$\u20B9-449]\\ #,##0.00", - 274: "[$\u20B9-820]#,##0.00", - 275: "#,##0.00\\ [$\u20BA-41F]", - 276: "#,##0.00\\ [$\u20BC-42C]", - 277: "#,##0.00\\ [$\u20BC-82C]", - 278: "#,##0.00\\ [$\u20BD-419]", - 279: "#,##0.00[$\u20BD-485]", - 280: "#,##0.00\\ [$\u20BE-437]", - 281: "[$B/.-180A]\\ #,##0.00", - 282: "[$Br-472]#,##0.00", - 283: "[$Br-477]#,##0.00", - 284: "#,##0.00[$Br-473]", - 285: "[$Bs-46B]\\ #,##0.00", - 286: "[$Bs-400A]\\ #,##0.00", - 287: "[$Bs.-200A]\\ #,##0.00", - 288: "[$BWP-832]\\ #,##0.00", - 289: "[$C$-4C0A]#,##0.00", - 290: "[$CA$-85D]#,##0.00", - 291: "[$CA$-47C]#,##0.00", - 292: "[$CA$-45D]#,##0.00", - 293: "[$CFA-340C]#,##0.00", - 294: "[$CFA-280C]#,##0.00", - 295: "#,##0.00\\ [$CFA-867]", - 296: "#,##0.00\\ [$CFA-488]", - 297: "#,##0.00\\ [$CHF-100C]", - 298: "[$CHF-1407]\\ #,##0.00", - 299: "[$CHF-807]\\ #,##0.00", - 300: "[$CHF-810]\\ #,##0.00", - 301: "[$CHF-417]\\ #,##0.00", - 302: "[$CLP-47A]\\ #,##0.00", - 303: "[$CN¥-850]#,##0.00", - 304: "#,##0.00\\ [$DZD-85F]", - 305: "[$FCFA-2C0C]#,##0.00", - 306: "#,##0.00\\ [$Ft-40E]", - 307: "[$G-3C0C]#,##0.00", - 308: "[$Gs.-3C0A]\\ #,##0.00", - 309: "[$GTQ-486]#,##0.00", - 310: "[$HK$-C04]#,##0.00", - 311: "[$HK$-3C09]#,##0.00", - 312: "#,##0.00\\ [$HRK-41A]", - 313: "[$IDR-3809]#,##0.00", - 314: "[$IQD-492]#,##0.00", - 315: "#,##0.00\\ [$ISK-40F]", - 316: "[$K-455]#,##0.00", - 317: "#,##0.00\\ [$K\u010D-405]", - 318: "#,##0.00\\ [$KM-141A]", - 319: "#,##0.00\\ [$KM-101A]", - 320: "#,##0.00\\ [$KM-181A]", - 321: "[$kr-438]\\ #,##0.00", - 322: "[$kr-43B]\\ #,##0.00", - 323: "#,##0.00\\ [$kr-83B]", - 324: "[$kr-414]\\ #,##0.00", - 325: "[$kr-814]\\ #,##0.00", - 326: "#,##0.00\\ [$kr-41D]", - 327: "[$kr.-406]\\ #,##0.00", - 328: "[$kr.-46F]\\ #,##0.00", - 329: "[$Ksh-441]#,##0.00", - 330: "[$L-818]#,##0.00", - 331: "[$L-819]#,##0.00", - 332: "[$L-480A]\\ #,##0.00", - 333: "#,##0.00\\ [$Lek\u00EB-41C]", - 334: "[$MAD-45F]#,##0.00", - 335: "[$MAD-380C]#,##0.00", - 336: "#,##0.00\\ [$MAD-105F]", - 337: "[$MOP$-1404]#,##0.00", - 338: "#,##0.00\\ [$MVR-465]_-", - 339: "#,##0.00[$Nfk-873]", - 340: "[$NGN-466]#,##0.00", - 341: "[$NGN-467]#,##0.00", - 342: "[$NGN-469]#,##0.00", - 343: "[$NGN-471]#,##0.00", - 344: "[$NOK-103B]\\ #,##0.00", - 345: "[$NOK-183B]\\ #,##0.00", - 346: "[$NZ$-481]#,##0.00", - 347: "[$PKR-859]\\ #,##0.00", - 348: "[$PYG-474]#,##0.00", - 349: "[$Q-100A]#,##0.00", - 350: "[$R-436]\\ #,##0.00", - 351: "[$R-1C09]\\ #,##0.00", - 352: "[$R-435]\\ #,##0.00", - 353: "[$R$-416]\\ #,##0.00", - 354: "[$RD$-1C0A]#,##0.00", - 355: "#,##0.00\\ [$RF-487]", - 356: "[$RM-4409]#,##0.00", - 357: "[$RM-43E]#,##0.00", - 358: "#,##0.00\\ [$RON-418]", - 359: "[$Rp-421]#,##0.00", - 360: "[$Rs-420]#,##0.00_-", - 361: "[$Rs.-849]\\ #,##0.00", - 362: "#,##0.00\\ [$RSD-81A]", - 363: "#,##0.00\\ [$RSD-C1A]", - 364: "#,##0.00\\ [$RUB-46D]", - 365: "#,##0.00\\ [$RUB-444]", - 366: "[$S/.-C6B]\\ #,##0.00", - 367: "[$S/.-280A]\\ #,##0.00", - 368: "#,##0.00\\ [$SEK-143B]", - 369: "#,##0.00\\ [$SEK-1C3B]", - 370: "#,##0.00\\ [$so\u02BBm-443]", - 371: "#,##0.00\\ [$so\u02BBm-843]", - 372: "#,##0.00\\ [$SYP-45A]", - 373: "[$THB-41E]#,##0.00", - 374: "#,##0.00[$TMT-442]", - 375: "[$US$-3009]#,##0.00", - 376: "[$ZAR-46C]\\ #,##0.00", - 377: "[$ZAR-430]#,##0.00", - 378: "[$ZAR-431]#,##0.00", - 379: "[$ZAR-432]\\ #,##0.00", - 380: "[$ZAR-433]#,##0.00", - 381: "[$ZAR-434]\\ #,##0.00", - 382: "#,##0.00\\ [$z\u0142-415]", - 383: "#,##0.00\\ [$\u0434\u0435\u043D-42F]", - 384: "#,##0.00\\ [$КМ-201A]", - 385: "#,##0.00\\ [$КМ-1C1A]", - 386: "#,##0.00\\ [$\u043B\u0432.-402]", - 387: "#,##0.00\\ [$р.-423]", - 388: "#,##0.00\\ [$\u0441\u043E\u043C-440]", - 389: "#,##0.00\\ [$\u0441\u043E\u043C-428]", - 390: "[$\u062C.\u0645.-C01]\\ #,##0.00_-", - 391: "[$\u062F.\u0623.-2C01]\\ #,##0.00_-", - 392: "[$\u062F.\u0625.-3801]\\ #,##0.00_-", - 393: "[$\u062F.\u0628.-3C01]\\ #,##0.00_-", - 394: "[$\u062F.\u062A.-1C01]\\ #,##0.00_-", - 395: "[$\u062F.\u062C.-1401]\\ #,##0.00_-", - 396: "[$\u062F.\u0639.-801]\\ #,##0.00_-", - 397: "[$\u062F.\u0643.-3401]\\ #,##0.00_-", - 398: "[$\u062F.\u0644.-1001]#,##0.00_-", - 399: "[$\u062F.\u0645.-1801]\\ #,##0.00_-", - 400: "[$\u0631-846]\\ #,##0.00", - 401: "[$\u0631.\u0633.-401]\\ #,##0.00_-", - 402: "[$\u0631.\u0639.-2001]\\ #,##0.00_-", - 403: "[$\u0631.\u0642.-4001]\\ #,##0.00_-", - 404: "[$\u0631.\u064A.-2401]\\ #,##0.00_-", - 405: "[$\u0631\u06CC\u0627\u0644-429]#,##0.00_-", - 406: "[$\u0644.\u0633.-2801]\\ #,##0.00_-", - 407: "[$\u0644.\u0644.-3001]\\ #,##0.00_-", - 408: "[$\u1265\u122D-45E]#,##0.00", - 409: "[$\u0930\u0942-461]#,##0.00", - 410: "[$\u0DBB\u0DD4.-45B]\\ #,##0.00", - 411: "[$ADP]\\ #,##0.00", - 412: "[$AED]\\ #,##0.00", - 413: "[$AFA]\\ #,##0.00", - 414: "[$AFN]\\ #,##0.00", - 415: "[$ALL]\\ #,##0.00", - 416: "[$AMD]\\ #,##0.00", - 417: "[$ANG]\\ #,##0.00", - 418: "[$AOA]\\ #,##0.00", - 419: "[$ARS]\\ #,##0.00", - 420: "[$ATS]\\ #,##0.00", - 421: "[$AUD]\\ #,##0.00", - 422: "[$AWG]\\ #,##0.00", - 423: "[$AZM]\\ #,##0.00", - 424: "[$AZN]\\ #,##0.00", - 425: "[$BAM]\\ #,##0.00", - 426: "[$BBD]\\ #,##0.00", - 427: "[$BDT]\\ #,##0.00", - 428: "[$BEF]\\ #,##0.00", - 429: "[$BGL]\\ #,##0.00", - 430: "[$BGN]\\ #,##0.00", - 431: "[$BHD]\\ #,##0.00", - 432: "[$BIF]\\ #,##0.00", - 433: "[$BMD]\\ #,##0.00", - 434: "[$BND]\\ #,##0.00", - 435: "[$BOB]\\ #,##0.00", - 436: "[$BOV]\\ #,##0.00", - 437: "[$BRL]\\ #,##0.00", - 438: "[$BSD]\\ #,##0.00", - 439: "[$BTN]\\ #,##0.00", - 440: "[$BWP]\\ #,##0.00", - 441: "[$BYR]\\ #,##0.00", - 442: "[$BZD]\\ #,##0.00", - 443: "[$CAD]\\ #,##0.00", - 444: "[$CDF]\\ #,##0.00", - 445: "[$CHE]\\ #,##0.00", - 446: "[$CHF]\\ #,##0.00", - 447: "[$CHW]\\ #,##0.00", - 448: "[$CLF]\\ #,##0.00", - 449: "[$CLP]\\ #,##0.00", - 450: "[$CNY]\\ #,##0.00", - 451: "[$COP]\\ #,##0.00", - 452: "[$COU]\\ #,##0.00", - 453: "[$CRC]\\ #,##0.00", - 454: "[$CSD]\\ #,##0.00", - 455: "[$CUC]\\ #,##0.00", - 456: "[$CVE]\\ #,##0.00", - 457: "[$CYP]\\ #,##0.00", - 458: "[$CZK]\\ #,##0.00", - 459: "[$DEM]\\ #,##0.00", - 460: "[$DJF]\\ #,##0.00", - 461: "[$DKK]\\ #,##0.00", - 462: "[$DOP]\\ #,##0.00", - 463: "[$DZD]\\ #,##0.00", - 464: "[$ECS]\\ #,##0.00", - 465: "[$ECV]\\ #,##0.00", - 466: "[$EEK]\\ #,##0.00", - 467: "[$EGP]\\ #,##0.00", - 468: "[$ERN]\\ #,##0.00", - 469: "[$ESP]\\ #,##0.00", - 470: "[$ETB]\\ #,##0.00", - 471: "[$EUR]\\ #,##0.00", - 472: "[$FIM]\\ #,##0.00", - 473: "[$FJD]\\ #,##0.00", - 474: "[$FKP]\\ #,##0.00", - 475: "[$FRF]\\ #,##0.00", - 476: "[$GBP]\\ #,##0.00", - 477: "[$GEL]\\ #,##0.00", - 478: "[$GHC]\\ #,##0.00", - 479: "[$GHS]\\ #,##0.00", - 480: "[$GIP]\\ #,##0.00", - 481: "[$GMD]\\ #,##0.00", - 482: "[$GNF]\\ #,##0.00", - 483: "[$GRD]\\ #,##0.00", - 484: "[$GTQ]\\ #,##0.00", - 485: "[$GYD]\\ #,##0.00", - 486: "[$HKD]\\ #,##0.00", - 487: "[$HNL]\\ #,##0.00", - 488: "[$HRK]\\ #,##0.00", - 489: "[$HTG]\\ #,##0.00", - 490: "[$HUF]\\ #,##0.00", - 491: "[$IDR]\\ #,##0.00", - 492: "[$IEP]\\ #,##0.00", - 493: "[$ILS]\\ #,##0.00", - 494: "[$INR]\\ #,##0.00", - 495: "[$IQD]\\ #,##0.00", - 496: "[$IRR]\\ #,##0.00", - 497: "[$ISK]\\ #,##0.00", - 498: "[$ITL]\\ #,##0.00", - 499: "[$JMD]\\ #,##0.00", - 500: "[$JOD]\\ #,##0.00", - 501: "[$JPY]\\ #,##0.00", - 502: "[$KAF]\\ #,##0.00", - 503: "[$KES]\\ #,##0.00", - 504: "[$KGS]\\ #,##0.00", - 505: "[$KHR]\\ #,##0.00", - 506: "[$KMF]\\ #,##0.00", - 507: "[$KPW]\\ #,##0.00", - 508: "[$KRW]\\ #,##0.00", - 509: "[$KWD]\\ #,##0.00", - 510: "[$KYD]\\ #,##0.00", - 511: "[$KZT]\\ #,##0.00", - 512: "[$LAK]\\ #,##0.00", - 513: "[$LBP]\\ #,##0.00", - 514: "[$LKR]\\ #,##0.00", - 515: "[$LRD]\\ #,##0.00", - 516: "[$LSL]\\ #,##0.00", - 517: "[$LTL]\\ #,##0.00", - 518: "[$LUF]\\ #,##0.00", - 519: "[$LVL]\\ #,##0.00", - 520: "[$LYD]\\ #,##0.00", - 521: "[$MAD]\\ #,##0.00", - 522: "[$MDL]\\ #,##0.00", - 523: "[$MGA]\\ #,##0.00", - 524: "[$MGF]\\ #,##0.00", - 525: "[$MKD]\\ #,##0.00", - 526: "[$MMK]\\ #,##0.00", - 527: "[$MNT]\\ #,##0.00", - 528: "[$MOP]\\ #,##0.00", - 529: "[$MRO]\\ #,##0.00", - 530: "[$MTL]\\ #,##0.00", - 531: "[$MUR]\\ #,##0.00", - 532: "[$MVR]\\ #,##0.00", - 533: "[$MWK]\\ #,##0.00", - 534: "[$MXN]\\ #,##0.00", - 535: "[$MXV]\\ #,##0.00", - 536: "[$MYR]\\ #,##0.00", - 537: "[$MZM]\\ #,##0.00", - 538: "[$MZN]\\ #,##0.00", - 539: "[$NAD]\\ #,##0.00", - 540: "[$NGN]\\ #,##0.00", - 541: "[$NIO]\\ #,##0.00", - 542: "[$NLG]\\ #,##0.00", - 543: "[$NOK]\\ #,##0.00", - 544: "[$NPR]\\ #,##0.00", - 545: "[$NTD]\\ #,##0.00", - 546: "[$NZD]\\ #,##0.00", - 547: "[$OMR]\\ #,##0.00", - 548: "[$PAB]\\ #,##0.00", - 549: "[$PEN]\\ #,##0.00", - 550: "[$PGK]\\ #,##0.00", - 551: "[$PHP]\\ #,##0.00", - 552: "[$PKR]\\ #,##0.00", - 553: "[$PLN]\\ #,##0.00", - 554: "[$PTE]\\ #,##0.00", - 555: "[$PYG]\\ #,##0.00", - 556: "[$QAR]\\ #,##0.00", - 557: "[$ROL]\\ #,##0.00", - 558: "[$RON]\\ #,##0.00", - 559: "[$RSD]\\ #,##0.00", - 560: "[$RUB]\\ #,##0.00", - 561: "[$RUR]\\ #,##0.00", - 562: "[$RWF]\\ #,##0.00", - 563: "[$SAR]\\ #,##0.00", - 564: "[$SBD]\\ #,##0.00", - 565: "[$SCR]\\ #,##0.00", - 566: "[$SDD]\\ #,##0.00", - 567: "[$SDG]\\ #,##0.00", - 568: "[$SDP]\\ #,##0.00", - 569: "[$SEK]\\ #,##0.00", - 570: "[$SGD]\\ #,##0.00", - 571: "[$SHP]\\ #,##0.00", - 572: "[$SIT]\\ #,##0.00", - 573: "[$SKK]\\ #,##0.00", - 574: "[$SLL]\\ #,##0.00", - 575: "[$SOS]\\ #,##0.00", - 576: "[$SPL]\\ #,##0.00", - 577: "[$SRD]\\ #,##0.00", - 578: "[$SRG]\\ #,##0.00", - 579: "[$STD]\\ #,##0.00", - 580: "[$SVC]\\ #,##0.00", - 581: "[$SYP]\\ #,##0.00", - 582: "[$SZL]\\ #,##0.00", - 583: "[$THB]\\ #,##0.00", - 584: "[$TJR]\\ #,##0.00", - 585: "[$TJS]\\ #,##0.00", - 586: "[$TMM]\\ #,##0.00", - 587: "[$TMT]\\ #,##0.00", - 588: "[$TND]\\ #,##0.00", - 589: "[$TOP]\\ #,##0.00", - 590: "[$TRL]\\ #,##0.00", - 591: "[$TRY]\\ #,##0.00", - 592: "[$TTD]\\ #,##0.00", - 593: "[$TWD]\\ #,##0.00", - 594: "[$TZS]\\ #,##0.00", - 595: "[$UAH]\\ #,##0.00", - 596: "[$UGX]\\ #,##0.00", - 597: "[$USD]\\ #,##0.00", - 598: "[$USN]\\ #,##0.00", - 599: "[$USS]\\ #,##0.00", - 600: "[$UYI]\\ #,##0.00", - 601: "[$UYU]\\ #,##0.00", - 602: "[$UZS]\\ #,##0.00", - 603: "[$VEB]\\ #,##0.00", - 604: "[$VEF]\\ #,##0.00", - 605: "[$VND]\\ #,##0.00", - 606: "[$VUV]\\ #,##0.00", - 607: "[$WST]\\ #,##0.00", - 608: "[$XAF]\\ #,##0.00", - 609: "[$XAG]\\ #,##0.00", - 610: "[$XAU]\\ #,##0.00", - 611: "[$XB5]\\ #,##0.00", - 612: "[$XBA]\\ #,##0.00", - 613: "[$XBB]\\ #,##0.00", - 614: "[$XBC]\\ #,##0.00", - 615: "[$XBD]\\ #,##0.00", - 616: "[$XCD]\\ #,##0.00", - 617: "[$XDR]\\ #,##0.00", - 618: "[$XFO]\\ #,##0.00", - 619: "[$XFU]\\ #,##0.00", - 620: "[$XOF]\\ #,##0.00", - 621: "[$XPD]\\ #,##0.00", - 622: "[$XPF]\\ #,##0.00", - 623: "[$XPT]\\ #,##0.00", - 624: "[$XTS]\\ #,##0.00", - 625: "[$XXX]\\ #,##0.00", - 626: "[$YER]\\ #,##0.00", - 627: "[$YUM]\\ #,##0.00", - 628: "[$ZAR]\\ #,##0.00", - 629: "[$ZMK]\\ #,##0.00", - 630: "[$ZMW]\\ #,##0.00", - 631: "[$ZWD]\\ #,##0.00", - 632: "[$ZWL]\\ #,##0.00", - 633: "[$ZWN]\\ #,##0.00", - 634: "[$ZWR]\\ #,##0.00", -} - // validType defined the list of valid validation types. var validType = map[string]string{ "cell": "cellIs", @@ -1086,56 +358,6 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // 57 | yyyy"年"m"月 // 58 | m"月"d"日" // -// Number format code with unicode values provided for language glyphs where -// they occur in zh-tw language: -// -// Index | Symbol -// -------+------------------------------------------- -// 27 | [$-404]e/m/ -// 28 | [$-404]e"5E74"m"6708"d"65E5 -// 29 | [$-404]e"5E74"m"6708"d"65E5 -// 30 | m/d/y -// 31 | yyyy"5E74"m"6708"d"65E5 -// 32 | hh"6642"mm"5206 -// 33 | hh"6642"mm"5206"ss"79D2 -// 34 | 4E0A5348/4E0B5348hh"6642"mm"5206 -// 35 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2 -// 36 | [$-404]e/m/ -// 50 | [$-404]e/m/ -// 51 | [$-404]e"5E74"m"6708"d"65E5 -// 52 | 4E0A5348/4E0B5348hh"6642"mm"5206 -// 53 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2 -// 54 | [$-404]e"5E74"m"6708"d"65E5 -// 55 | 4E0A5348/4E0B5348hh"6642"mm"5206 -// 56 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2 -// 57 | [$-404]e/m/ -// 58 | [$-404]e"5E74"m"6708"d"65E5" -// -// Number format code with unicode values provided for language glyphs where -// they occur in zh-cn language: -// -// Index | Symbol -// -------+------------------------------------------- -// 27 | yyyy"5E74"m"6708 -// 28 | m"6708"d"65E5 -// 29 | m"6708"d"65E5 -// 30 | m-d-y -// 31 | yyyy"5E74"m"6708"d"65E5 -// 32 | h"65F6"mm"5206 -// 33 | h"65F6"mm"5206"ss"79D2 -// 34 | 4E0A5348/4E0B5348h"65F6"mm"5206 -// 35 | 4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2 -// 36 | yyyy"5E74"m"6708 -// 50 | yyyy"5E74"m"6708 -// 51 | m"6708"d"65E5 -// 52 | yyyy"5E74"m"6708 -// 53 | m"6708"d"65E5 -// 54 | m"6708"d"65E5 -// 55 | 4E0A5348/4E0B5348h"65F6"mm"5206 -// 56 | 4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2 -// 57 | yyyy"5E74"m"6708 -// 58 | m"6708"d"65E5" -// // Number format code in ja-jp language: // // Index | Symbol @@ -1184,56 +406,6 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // 57 | yyyy"年" mm"月" dd"日 // 58 | mm-dd // -// Number format code with unicode values provided for language glyphs where -// they occur in ja-jp language: -// -// Index | Symbol -// -------+------------------------------------------- -// 27 | [$-411]ge.m.d -// 28 | [$-411]ggge"5E74"m"6708"d"65E5 -// 29 | [$-411]ggge"5E74"m"6708"d"65E5 -// 30 | m/d/y -// 31 | yyyy"5E74"m"6708"d"65E5 -// 32 | h"6642"mm"5206 -// 33 | h"6642"mm"5206"ss"79D2 -// 34 | yyyy"5E74"m"6708 -// 35 | m"6708"d"65E5 -// 36 | [$-411]ge.m.d -// 50 | [$-411]ge.m.d -// 51 | [$-411]ggge"5E74"m"6708"d"65E5 -// 52 | yyyy"5E74"m"6708 -// 53 | m"6708"d"65E5 -// 54 | [$-411]ggge"5E74"m"6708"d"65E5 -// 55 | yyyy"5E74"m"6708 -// 56 | m"6708"d"65E5 -// 57 | [$-411]ge.m.d -// 58 | [$-411]ggge"5E74"m"6708"d"65E5" -// -// Number format code with unicode values provided for language glyphs where -// they occur in ko-kr language: -// -// Index | Symbol -// -------+------------------------------------------- -// 27 | yyyy"5E74" mm"6708" dd"65E5 -// 28 | mm-d -// 29 | mm-d -// 30 | mm-dd-y -// 31 | yyyy"B144" mm"C6D4" dd"C77C -// 32 | h"C2DC" mm"BD84 -// 33 | h"C2DC" mm"BD84" ss"CD08 -// 34 | yyyy-mm-d -// 35 | yyyy-mm-d -// 36 | yyyy"5E74" mm"6708" dd"65E5 -// 50 | yyyy"5E74" mm"6708" dd"65E5 -// 51 | mm-d -// 52 | yyyy-mm-d -// 53 | yyyy-mm-d -// 54 | mm-d -// 55 | yyyy-mm-d -// 56 | yyyy-mm-d -// 57 | yyyy"5E74" mm"6708" dd"65E5 -// 58 | mm-dd -// // Number format code in th-th language: // // Index | Symbol @@ -1258,31 +430,6 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // 80 | นน:ทท. // 81 | d/m/bb // -// Number format code with unicode values provided for language glyphs where -// they occur in th-th language: -// -// Index | Symbol -// -------+------------------------------------------- -// 59 | t -// 60 | t0.0 -// 61 | t#,## -// 62 | t#,##0.0 -// 67 | t0 -// 68 | t0.00 -// 69 | t# ?/ -// 70 | t# ??/? -// 71 | 0E27/0E14/0E1B0E1B0E1B0E1 -// 72 | 0E27-0E140E140E14-0E1B0E1 -// 73 | 0E27-0E140E140E1 -// 74 | 0E140E140E14-0E1B0E1 -// 75 | 0E0A:0E190E1 -// 76 | 0E0A:0E190E19:0E170E1 -// 77 | 0E27/0E14/0E1B0E1B0E1B0E1B 0E0A:0E190E1 -// 78 | 0E190E19:0E170E1 -// 79 | [0E0A]:0E190E19:0E170E1 -// 80 | 0E190E19:0E170E17. -// 81 | d/m/bb -// // Excelize built-in currency formats are shown in the following table, only // support these types in the following table (Index number is used only for // markup and is not used inside an Excel file and you can't get formatted value @@ -1858,7 +1005,7 @@ var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ if style.CustomNumFmt == nil && numFmtID == -1 { return xf.NumFmtID != nil && *xf.NumFmtID == 0 } - if style.NegRed || style.Lang != "" || style.DecimalPlaces != 2 { + if style.NegRed || style.DecimalPlaces != 2 { return false } return xf.NumFmtID != nil && *xf.NumFmtID == numFmtID @@ -2091,11 +1238,9 @@ func getNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (numFmtID int) { if _, ok := builtInNumFmt[style.NumFmt]; ok { return style.NumFmt } - for lang, numFmt := range langNumFmt { - if _, ok := numFmt[style.NumFmt]; ok && lang == style.Lang { - numFmtID = style.NumFmt - return - } + if (27 <= style.NumFmt && style.NumFmt <= 36) || (50 <= style.NumFmt && style.NumFmt <= 81) { + numFmtID = style.NumFmt + return } if fmtCode, ok := currencyNumFmt[style.NumFmt]; ok { numFmtID = style.NumFmt @@ -2199,29 +1344,10 @@ func getCustomNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (customNumFmtID // setLangNumFmt provides a function to set number format code with language. func setLangNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { - numFmts, ok := langNumFmt[style.Lang] - if !ok { - return 0 - } - var fc string - fc, ok = numFmts[style.NumFmt] - if !ok { - return 0 - } - nf := xlsxNumFmt{FormatCode: fc} - if styleSheet.NumFmts != nil { - nf.NumFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1 - styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf) - styleSheet.NumFmts.Count++ - } else { - nf.NumFmtID = style.NumFmt - numFmts := xlsxNumFmts{ - NumFmt: []*xlsxNumFmt{&nf}, - Count: 1, - } - styleSheet.NumFmts = &numFmts + if (27 <= style.NumFmt && style.NumFmt <= 36) || (50 <= style.NumFmt && style.NumFmt <= 81) { + return style.NumFmt } - return nf.NumFmtID + return 0 } // getFillID provides a function to get fill ID. If given fill is not diff --git a/styles_test.go b/styles_test.go index f8ca15e035..af9654fba6 100644 --- a/styles_test.go +++ b/styles_test.go @@ -291,9 +291,7 @@ func TestNewStyle(t *testing.T) { // Test create currency custom style f.Styles.NumFmts = nil styleID, err = f.NewStyle(&Style{ - Lang: "ko-kr", NumFmt: 32, // must not be in currencyNumFmt - }) assert.NoError(t, err) assert.Equal(t, 3, styleID) @@ -330,14 +328,14 @@ func TestNewStyle(t *testing.T) { f = NewFile() f.Styles.NumFmts = nil f.Styles.CellXfs.Xf = nil - style4, err := f.NewStyle(&Style{NumFmt: 160, Lang: "unknown"}) + style4, err := f.NewStyle(&Style{NumFmt: 160}) assert.NoError(t, err) assert.Equal(t, 0, style4) f = NewFile() f.Styles.NumFmts = nil f.Styles.CellXfs.Xf = nil - style5, err := f.NewStyle(&Style{NumFmt: 160, Lang: "zh-cn"}) + style5, err := f.NewStyle(&Style{NumFmt: 160}) assert.NoError(t, err) assert.Equal(t, 0, style5) diff --git a/xmlStyles.go b/xmlStyles.go index 437446ebed..9700919aa9 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -371,6 +371,5 @@ type Style struct { NumFmt int DecimalPlaces int CustomNumFmt *string - Lang string NegRed bool } From 1088302331564777336b90a8298edfdbc827d513 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 16 May 2023 09:44:08 +0800 Subject: [PATCH 194/213] This closes #1535, add documentation for the fields for style alignment --- styles.go | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/styles.go b/styles.go index 960aa89ee0..9dc142e5d5 100644 --- a/styles.go +++ b/styles.go @@ -162,9 +162,11 @@ func parseFormatStyleSet(style *Style) (*Style, error) { return style, err } -// NewStyle provides a function to create the style for cells by given style -// options. This function is concurrency safe. Note that the 'Font.Color' field -// uses an RGB color represented in 'RRGGBB' hexadecimal notation. +// NewStyle provides a function to create the style for cells by a given style +// options, and returns style index. The same style index can not be used +// across different workbook. This function is concurrency safe. Note that +// the 'Font.Color' field uses an RGB color represented in 'RRGGBB' hexadecimal +// notation. // // The following table shows the border types used in 'Border.Type' supported by // excelize: @@ -236,6 +238,18 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // 8 | darkUp | 18 | gray0625 // 9 | darkGrid | | // +// The 'Alignment.Indent' is an integer value, where an increment of 1 +// represents 3 spaces. Indicates the number of spaces (of the normal style +// font) of indentation for text in a cell. The number of spaces to indent is +// calculated as following: +// +// Number of spaces to indent = indent value * 3 +// +// For example, an indent value of 1 means that the text begins 3 space widths +// (of the normal style font) from the edge of the cell. Note: The width of one +// space character is defined by the font. Only left, right, and distributed +// horizontal alignments are supported. +// // The following table shows the type of cells' horizontal alignment used // in 'Alignment.Horizontal': // @@ -259,6 +273,24 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // justify // distributed // +// The 'Alignment.ReadingOrder' is an uint64 value indicating whether the +// reading order of the cell is left-to-right, right-to-left, or context +// dependent. the valid value of this field was: +// +// Value | Description +// -------+---------------------------------------------------- +// 0 | Context Dependent - reading order is determined by scanning the +// | text for the first non-whitespace character: if it is a strong +// | right-to-left character, the reading order is right-to-left; +// | otherwise, the reading order left-to-right. +// 1 | Left-to-Right: reading order is left-to-right in the cell, as in +// | English. +// 2 | Right-to-Left: reading order is right-to-left in the cell, as in +// | Hebrew. +// +// The 'Alignment.RelativeIndent' is an integer value to indicate the additional +// number of spaces of indentation to adjust for text in a cell. +// // The following table shows the type of font underline style used in // 'Font.Underline': // From ef3e81de8e7429d6601d05a19dda17180d97ef53 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 17 May 2023 00:05:27 +0800 Subject: [PATCH 195/213] This fixed across worksheet reference issue for the formula calculation engine --- calc.go | 178 ++++++++++++++++++++++++--------------------------- calc_test.go | 48 +++++++++----- 2 files changed, 118 insertions(+), 108 deletions(-) diff --git a/calc.go b/calc.go index 96abd64c6e..402c7deeb1 100644 --- a/calc.go +++ b/calc.go @@ -951,9 +951,6 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T if err != nil { return result, err } - if result.Type == ArgError { - return result, errors.New(result.Error) - } opfdStack.Push(result) continue } @@ -965,10 +962,7 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T } result, err := f.parseReference(ctx, sheet, token.TValue) if err != nil { - return newEmptyFormulaArg(), err - } - if result.Type == ArgUnknown { - return newEmptyFormulaArg(), errors.New(formulaErrorVALUE) + return result, err } // when current token is range, next token is argument and opfdStack not empty, // should push value to opfdStack and continue @@ -1442,74 +1436,99 @@ func (f *File) parseToken(ctx *calcContext, sheet string, token efp.Token, opdSt return nil } +// parseRef parse reference for a cell, column name or row number. +func (f *File) parseRef(ref string) (cellRef, bool, bool, error) { + var ( + err, colErr, rowErr error + cr cellRef + cell = ref + tokens = strings.Split(ref, "!") + ) + if len(tokens) == 2 { // have a worksheet + cr.Sheet, cell = tokens[0], tokens[1] + } + if cr.Col, cr.Row, err = CellNameToCoordinates(cell); err != nil { + if cr.Col, colErr = ColumnNameToNumber(cell); colErr == nil { // cast to column + return cr, true, false, nil + } + if cr.Row, rowErr = strconv.Atoi(cell); rowErr == nil { // cast to row + return cr, false, true, nil + } + return cr, false, false, err + } + return cr, false, false, err +} + +// prepareCellRange checking and convert cell reference to a cell range. +func (cr *cellRange) prepareCellRange(col, row bool, cellRef cellRef) error { + if col { + cellRef.Row = TotalRows + } + if row { + cellRef.Col = MaxColumns + } + if cellRef.Sheet == "" { + cellRef.Sheet = cr.From.Sheet + } + if cr.From.Sheet != cellRef.Sheet || cr.To.Sheet != cellRef.Sheet { + return errors.New("invalid reference") + } + if cr.From.Col > cellRef.Col { + cr.From.Col = cellRef.Col + } + if cr.From.Row > cellRef.Row { + cr.From.Row = cellRef.Row + } + if cr.To.Col < cellRef.Col { + cr.To.Col = cellRef.Col + } + if cr.To.Row < cellRef.Row { + cr.To.Row = cellRef.Row + } + return nil +} + // parseReference parse reference and extract values by given reference // characters and default sheet name. -func (f *File) parseReference(ctx *calcContext, sheet, reference string) (arg formulaArg, err error) { +func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formulaArg, error) { reference = strings.ReplaceAll(reference, "$", "") - refs, cellRanges, cellRefs := list.New(), list.New(), list.New() - for _, ref := range strings.Split(reference, ":") { - tokens := strings.Split(ref, "!") - cr := cellRef{} - if len(tokens) == 2 { // have a worksheet name - cr.Sheet = tokens[0] - // cast to cell reference - if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[1]); err != nil { - // cast to column - if cr.Col, err = ColumnNameToNumber(tokens[1]); err != nil { - // cast to row - if cr.Row, err = strconv.Atoi(tokens[1]); err != nil { - err = newInvalidColumnNameError(tokens[1]) - return - } - cr.Col = MaxColumns - } - } - if refs.Len() > 0 { - e := refs.Back() - cellRefs.PushBack(e.Value.(cellRef)) - refs.Remove(e) + ranges, cellRanges, cellRefs := strings.Split(reference, ":"), list.New(), list.New() + if len(ranges) > 1 { + var cr cellRange + for i, ref := range ranges { + cellRef, col, row, err := f.parseRef(ref) + if err != nil { + return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference") } - refs.PushBack(cr) - continue - } - // cast to cell reference - if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[0]); err != nil { - // cast to column - if cr.Col, err = ColumnNameToNumber(tokens[0]); err != nil { - // cast to row - if cr.Row, err = strconv.Atoi(tokens[0]); err != nil { - err = newInvalidColumnNameError(tokens[0]) - return + if i == 0 { + if col { + cellRef.Row = 1 + } + if row { + cellRef.Col = 1 } - cr.Col = MaxColumns + if cellRef.Sheet == "" { + cellRef.Sheet = sheet + } + cr.From, cr.To = cellRef, cellRef + continue + } + if err := cr.prepareCellRange(col, row, cellRef); err != nil { + return newErrorFormulaArg(formulaErrorNAME, err.Error()), err } - cellRanges.PushBack(cellRange{ - From: cellRef{Sheet: sheet, Col: cr.Col, Row: 1}, - To: cellRef{Sheet: sheet, Col: cr.Col, Row: TotalRows}, - }) - cellRefs.Init() - arg, err = f.rangeResolver(ctx, cellRefs, cellRanges) - return - } - e := refs.Back() - if e == nil { - cr.Sheet = sheet - refs.PushBack(cr) - continue } - cellRanges.PushBack(cellRange{ - From: e.Value.(cellRef), - To: cr, - }) - refs.Remove(e) + cellRanges.PushBack(cr) + return f.rangeResolver(ctx, cellRefs, cellRanges) } - if refs.Len() > 0 { - e := refs.Back() - cellRefs.PushBack(e.Value.(cellRef)) - refs.Remove(e) + cellRef, _, _, err := f.parseRef(reference) + if err != nil { + return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference") } - arg, err = f.rangeResolver(ctx, cellRefs, cellRanges) - return + if cellRef.Sheet == "" { + cellRef.Sheet = sheet + } + cellRefs.PushBack(cellRef) + return f.rangeResolver(ctx, cellRefs, cellRanges) } // prepareValueRange prepare value range. @@ -1598,9 +1617,6 @@ func (f *File) rangeResolver(ctx *calcContext, cellRefs, cellRanges *list.List) // prepare value range for temp := cellRanges.Front(); temp != nil; temp = temp.Next() { cr := temp.Value.(cellRange) - if cr.From.Sheet != cr.To.Sheet { - err = errors.New(formulaErrorVALUE) - } rng := []int{cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row} _ = sortCoordinates(rng) cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row = rng[0], rng[1], rng[2], rng[3] @@ -14155,18 +14171,9 @@ func calcColumnsMinMax(argsList *list.List) (min, max int) { if min == 0 { min = cr.Value.(cellRange).From.Col } - if min > cr.Value.(cellRange).From.Col { - min = cr.Value.(cellRange).From.Col - } - if min > cr.Value.(cellRange).To.Col { - min = cr.Value.(cellRange).To.Col - } if max < cr.Value.(cellRange).To.Col { max = cr.Value.(cellRange).To.Col } - if max < cr.Value.(cellRange).From.Col { - max = cr.Value.(cellRange).From.Col - } } } if argsList.Front().Value.(formulaArg).cellRefs != nil && argsList.Front().Value.(formulaArg).cellRefs.Len() > 0 { @@ -14175,9 +14182,6 @@ func calcColumnsMinMax(argsList *list.List) (min, max int) { if min == 0 { min = refs.Value.(cellRef).Col } - if min > refs.Value.(cellRef).Col { - min = refs.Value.(cellRef).Col - } if max < refs.Value.(cellRef).Col { max = refs.Value.(cellRef).Col } @@ -14936,18 +14940,9 @@ func calcRowsMinMax(argsList *list.List) (min, max int) { if min == 0 { min = cr.Value.(cellRange).From.Row } - if min > cr.Value.(cellRange).From.Row { - min = cr.Value.(cellRange).From.Row - } - if min > cr.Value.(cellRange).To.Row { - min = cr.Value.(cellRange).To.Row - } if max < cr.Value.(cellRange).To.Row { max = cr.Value.(cellRange).To.Row } - if max < cr.Value.(cellRange).From.Row { - max = cr.Value.(cellRange).From.Row - } } } if argsList.Front().Value.(formulaArg).cellRefs != nil && argsList.Front().Value.(formulaArg).cellRefs.Len() > 0 { @@ -14956,9 +14951,6 @@ func calcRowsMinMax(argsList *list.List) (min, max int) { if min == 0 { min = refs.Value.(cellRef).Row } - if min > refs.Value.(cellRef).Row { - min = refs.Value.(cellRef).Row - } if max < refs.Value.(cellRef).Row { max = refs.Value.(cellRef).Row } diff --git a/calc_test.go b/calc_test.go index 1cb1580d08..b9c9a8d87b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2409,7 +2409,7 @@ func TestCalcCellValue(t *testing.T) { // ABS "=ABS()": {"#VALUE!", "ABS requires 1 numeric argument"}, "=ABS(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, - "=ABS(~)": {"", newInvalidColumnNameError("~").Error()}, + "=ABS(~)": {"#NAME?", "invalid reference"}, // ACOS "=ACOS()": {"#VALUE!", "ACOS requires 1 numeric argument"}, "=ACOS(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, @@ -3794,17 +3794,19 @@ func TestCalcCellValue(t *testing.T) { "=CHOOSE(2,0)": {"#VALUE!", "index_num should be <= to the number of values"}, "=CHOOSE(1,NA())": {"#N/A", "#N/A"}, // COLUMN - "=COLUMN(1,2)": {"#VALUE!", "COLUMN requires at most 1 argument"}, - "=COLUMN(\"\")": {"#VALUE!", "invalid reference"}, - "=COLUMN(Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, - "=COLUMN(Sheet1!A1!B1)": {"", newInvalidColumnNameError("Sheet1").Error()}, + "=COLUMN(1,2)": {"#VALUE!", "COLUMN requires at most 1 argument"}, + "=COLUMN(\"\")": {"#VALUE!", "invalid reference"}, + "=COLUMN(Sheet1)": {"#NAME?", "invalid reference"}, + "=COLUMN(Sheet1!A1!B1)": {"#NAME?", "invalid reference"}, + "=COLUMN(Sheet1!A1:Sheet2!A2)": {"#NAME?", "invalid reference"}, + "=COLUMN(Sheet1!A1:1A)": {"#NAME?", "invalid reference"}, // COLUMNS "=COLUMNS()": {"#VALUE!", "COLUMNS requires 1 argument"}, "=COLUMNS(1)": {"#VALUE!", "invalid reference"}, "=COLUMNS(\"\")": {"#VALUE!", "invalid reference"}, - "=COLUMNS(Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, - "=COLUMNS(Sheet1!A1!B1)": {"", newInvalidColumnNameError("Sheet1").Error()}, - "=COLUMNS(Sheet1!Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, + "=COLUMNS(Sheet1)": {"#NAME?", "invalid reference"}, + "=COLUMNS(Sheet1!A1!B1)": {"#NAME?", "invalid reference"}, + "=COLUMNS(Sheet1!Sheet1)": {"#NAME?", "invalid reference"}, // FORMULATEXT "=FORMULATEXT()": {"#VALUE!", "FORMULATEXT requires 1 argument"}, "=FORMULATEXT(1)": {"#VALUE!", "#VALUE!"}, @@ -3874,15 +3876,15 @@ func TestCalcCellValue(t *testing.T) { // ROW "=ROW(1,2)": {"#VALUE!", "ROW requires at most 1 argument"}, "=ROW(\"\")": {"#VALUE!", "invalid reference"}, - "=ROW(Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, - "=ROW(Sheet1!A1!B1)": {"", newInvalidColumnNameError("Sheet1").Error()}, + "=ROW(Sheet1)": {"#NAME?", "invalid reference"}, + "=ROW(Sheet1!A1!B1)": {"#NAME?", "invalid reference"}, // ROWS "=ROWS()": {"#VALUE!", "ROWS requires 1 argument"}, "=ROWS(1)": {"#VALUE!", "invalid reference"}, "=ROWS(\"\")": {"#VALUE!", "invalid reference"}, - "=ROWS(Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, - "=ROWS(Sheet1!A1!B1)": {"", newInvalidColumnNameError("Sheet1").Error()}, - "=ROWS(Sheet1!Sheet1)": {"", newInvalidColumnNameError("Sheet1").Error()}, + "=ROWS(Sheet1)": {"#NAME?", "invalid reference"}, + "=ROWS(Sheet1!A1!B1)": {"#NAME?", "invalid reference"}, + "=ROWS(Sheet1!Sheet1)": {"#NAME?", "invalid reference"}, // Web Functions // ENCODEURL "=ENCODEURL()": {"#VALUE!", "ENCODEURL requires 1 argument"}, @@ -4376,6 +4378,7 @@ func TestCalcCellValue(t *testing.T) { // SUM "=A1/A3": "0.333333333333333", "=SUM(A1:A2)": "3", + "=SUM(Sheet1!A1:Sheet1!A2)": "3", "=SUM(Sheet1!A1,A2)": "3", "=(-2-SUM(-4+A2))*5": "0", "=SUM(Sheet1!A1:Sheet1!A1:A2,A2)": "5", @@ -5549,8 +5552,7 @@ func TestCalcSHEETS(t *testing.T) { assert.NoError(t, err) formulaList := map[string]string{ "=SHEETS(Sheet1!A1:B1)": "1", - "=SHEETS(Sheet1!A1:Sheet1!A1)": "1", - "=SHEETS(Sheet1!A1:Sheet2!A1)": "2", + "=SHEETS(Sheet1!A1:Sheet1!B1)": "1", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formula)) @@ -5905,3 +5907,19 @@ func TestCalcCellResolver(t *testing.T) { assert.Equal(t, expected, result, formula) } } + +func TestEvalInfixExp(t *testing.T) { + f := NewFile() + arg, err := f.evalInfixExp(nil, "Sheet1", "A1", []efp.Token{ + {TSubType: efp.TokenSubTypeRange, TValue: "1A"}, + }) + assert.Equal(t, arg, newEmptyFormulaArg()) + assert.Equal(t, formulaErrorNAME, err.Error()) +} + +func TestParseToken(t *testing.T) { + f := NewFile() + assert.Equal(t, formulaErrorNAME, f.parseToken(nil, "Sheet1", + efp.Token{TSubType: efp.TokenSubTypeRange, TValue: "1A"}, nil, nil, + ).Error()) +} From 08ba2723fe6978a3e62e55f5a8cd0f856b1ecb6f Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 18 May 2023 20:33:16 +0800 Subject: [PATCH 196/213] This closes #1536, support fallback to default column width in sheet format property --- col.go | 6 ++++++ col_test.go | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/col.go b/col.go index e396005b4f..4d19a2aa3e 100644 --- a/col.go +++ b/col.go @@ -657,6 +657,9 @@ func (f *File) getColWidth(sheet string, col int) int { return int(convertColWidthToPixels(width)) } } + if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 { + return int(convertColWidthToPixels(ws.SheetFormatPr.DefaultColWidth)) + } // Optimization for when the column widths haven't changed. return int(defaultColWidthPixels) } @@ -715,6 +718,9 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { return width, err } } + if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 { + return ws.SheetFormatPr.DefaultColWidth, err + } // Optimization for when the column widths haven't changed. return defaultColWidth, err } diff --git a/col_test.go b/col_test.go index 335bee0685..ce7c3808c4 100644 --- a/col_test.go +++ b/col_test.go @@ -366,6 +366,15 @@ func TestColWidth(t *testing.T) { assert.Equal(t, defaultColWidth, width) assert.NoError(t, err) + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetFormatPr = &xlsxSheetFormatPr{DefaultColWidth: 10} + ws.(*xlsxWorksheet).Cols = nil + width, err = f.GetColWidth("Sheet1", "A") + assert.NoError(t, err) + assert.Equal(t, 10.0, width) + assert.Equal(t, 76, f.getColWidth("Sheet1", 1)) + // Test set and get column width with illegal cell reference width, err = f.GetColWidth("Sheet1", "*") assert.Equal(t, defaultColWidth, width) From a246db6a401d959ad7ce10227408267b8d9caad2 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 19 May 2023 19:53:18 +0800 Subject: [PATCH 197/213] This closes #279, refs #1536, change the default point to pixels conversion factor --- chart_test.go | 2 +- rows.go | 11 ++++++----- xmlDrawing.go | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/chart_test.go b/chart_test.go index f57cb4c837..ba17dbd611 100644 --- a/chart_test.go +++ b/chart_test.go @@ -94,7 +94,7 @@ func TestChartSize(t *testing.T) { } if !assert.Equal(t, 14, anchor.To.Col, "Expected 'to' column 14") || - !assert.Equal(t, 27, anchor.To.Row, "Expected 'to' row 27") { + !assert.Equal(t, 29, anchor.To.Row, "Expected 'to' row 29") { t.FailNow() } diff --git a/rows.go b/rows.go index c08a56d5bc..60b74b3a90 100644 --- a/rows.go +++ b/rows.go @@ -380,6 +380,9 @@ func (f *File) getRowHeight(sheet string, row int) int { return int(convertRowHeightToPixels(*v.Ht)) } } + if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultRowHeight > 0 { + return int(convertRowHeightToPixels(ws.SheetFormatPr.DefaultRowHeight)) + } // Optimization for when the row heights haven't changed. return int(defaultRowHeightPixels) } @@ -390,7 +393,7 @@ func (f *File) getRowHeight(sheet string, row int) int { // height, err := f.GetRowHeight("Sheet1", 1) func (f *File) GetRowHeight(sheet string, row int) (float64, error) { if row < 1 { - return defaultRowHeightPixels, newInvalidRowNumberError(row) + return defaultRowHeight, newInvalidRowNumberError(row) } ht := defaultRowHeight ws, err := f.workSheetReader(sheet) @@ -840,10 +843,8 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error { // cell from user's units to pixels. If the height hasn't been set by the user // we use the default value. If the row is hidden it has a value of zero. func convertRowHeightToPixels(height float64) float64 { - var pixels float64 if height == 0 { - return pixels + return 0 } - pixels = math.Ceil(4.0 / 3.0 * height) - return pixels + return math.Ceil(4.0 / 3.4 * height) } diff --git a/xmlDrawing.go b/xmlDrawing.go index 9e7c48ea31..dae3bcc3e5 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -144,7 +144,7 @@ const ( pivotTableVersion = 3 defaultPictureScale = 1.0 defaultChartDimensionWidth = 480 - defaultChartDimensionHeight = 290 + defaultChartDimensionHeight = 260 defaultChartLegendPosition = "bottom" defaultChartShowBlanksAs = "gap" defaultShapeSize = 160 From c2327484006ef3b65d2c2b81f712d7a821393a46 Mon Sep 17 00:00:00 2001 From: Eng Zer Jun Date: Mon, 22 May 2023 09:24:28 +0800 Subject: [PATCH 198/213] This avoid unnecessary byte/string conversion (#1541) Signed-off-by: Eng Zer Jun --- numfmt.go | 2 +- table.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/numfmt.go b/numfmt.go index cea63a1ad9..57aa8c8ed1 100644 --- a/numfmt.go +++ b/numfmt.go @@ -1220,7 +1220,7 @@ func printCommaSep(text string) string { if i > 0 && (length-i)%3 == 0 { target.WriteString(",") } - target.WriteString(string(text[i])) + target.WriteByte(text[i]) } if len(subStr) == 2 { target.WriteString(".") diff --git a/table.go b/table.go index 8b375a8a3f..094f765927 100644 --- a/table.go +++ b/table.go @@ -490,7 +490,7 @@ func (f *File) parseFilterExpression(expression string, tokens []string) ([]int, // expressions). conditional := 0 c := tokens[3] - if conditionFormat.Match([]byte(c)) { + if conditionFormat.MatchString(c) { conditional = 1 } expression1, token1, err := f.parseFilterTokens(expression, tokens[:3]) @@ -538,7 +538,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str } token := tokens[2] // Special handling for Blanks/NonBlanks. - re := blankFormat.Match([]byte(strings.ToLower(token))) + re := blankFormat.MatchString((strings.ToLower(token))) if re { // Only allow Equals or NotEqual in this context. if operator != 2 && operator != 5 { @@ -563,7 +563,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str } // If the string token contains an Excel match character then change the // operator type to indicate a non "simple" equality. - re = matchFormat.Match([]byte(token)) + re = matchFormat.MatchString(token) if operator == 2 && re { operator = 22 } From 76cd0992b038ceaad87a471f81cbde503423cc85 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 23 May 2023 00:18:55 +0800 Subject: [PATCH 199/213] This closes #1539, fix adjust table issue when after removing rows --- adjust.go | 2 +- adjust_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/adjust.go b/adjust.go index 216f4c8c5b..7fc9faa48d 100644 --- a/adjust.go +++ b/adjust.go @@ -242,7 +242,7 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, } coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset) x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] - if y2-y1 < 2 || x2-x1 < 1 { + if y2-y1 < 1 || x2-x1 < 0 { ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...) ws.TableParts.Count = len(ws.TableParts.TableParts) idx-- diff --git a/adjust_test.go b/adjust_test.go index f55ef4b7eb..c90a3f5cc8 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -320,6 +320,7 @@ func TestAdjustTable(t *testing.T) { } assert.NoError(t, f.RemoveRow(sheetName, 2)) assert.NoError(t, f.RemoveRow(sheetName, 3)) + assert.NoError(t, f.RemoveRow(sheetName, 3)) assert.NoError(t, f.RemoveCol(sheetName, "H")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustTable.xlsx"))) From 16efeae5b1b67d9c46dc02a8c50de64532f7f407 Mon Sep 17 00:00:00 2001 From: joehan109 Date: Sat, 27 May 2023 00:22:35 +0800 Subject: [PATCH 200/213] This fix date and time pattern validation issues (#1547) --- numfmt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numfmt.go b/numfmt.go index 57aa8c8ed1..9e48a6e5ed 100644 --- a/numfmt.go +++ b/numfmt.go @@ -1049,7 +1049,7 @@ func (f *File) checkDateTimePattern() error { p := nfp.NumberFormatParser() for _, section := range p.Parse(pattern) { for _, token := range section.Items { - if inStrSlice(supportedNumberTokenTypes, token.TType, false) == -1 || inStrSlice(supportedNumberTokenTypes, token.TType, false) != -1 { + if inStrSlice(supportedTokenTypes, token.TType, false) == -1 || inStrSlice(supportedNumberTokenTypes, token.TType, false) != -1 { return ErrUnsupportedNumberFormat } } From e3fb2d7bad2fb72556a014da8d4f96e4294b896e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A3=B9=E6=AC=A1=E5=BF=83?= <45734708+yicixin@users.noreply.github.com> Date: Sun, 28 May 2023 00:46:34 +0800 Subject: [PATCH 201/213] This closes #1548, support to get multiple images in one cell (#1549) --- picture.go | 1 - 1 file changed, 1 deletion(-) diff --git a/picture.go b/picture.go index 6ee83595f4..c30d307b04 100644 --- a/picture.go +++ b/picture.go @@ -672,7 +672,6 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) pic.Format.AltText = deTwoCellAnchor.Pic.NvPicPr.CNvPr.Descr pics = append(pics, pic) } - return } } } From 121ac17ca0e5458d4915aa8743abc26fc56075c5 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 30 May 2023 00:14:44 +0800 Subject: [PATCH 202/213] This fixed incorrect formula calculation exception expected result - Simplify and remove duplicate code for optimization - Update documentation comments with typo fix - Handle error return to save the workbook - Add file path length limitation details in the error message --- calc.go | 313 ++++++++++++++++++-------------------------- calc_test.go | 6 +- date.go | 4 +- errors.go | 2 +- excelize.go | 6 +- hsl.go | 4 +- numfmt.go | 38 +++--- numfmt_test.go | 2 +- rows.go | 6 +- rows_test.go | 109 +++++---------- styles.go | 8 +- table.go | 4 +- xmlDecodeDrawing.go | 2 +- xmlStyles.go | 16 +-- xmlWorkbook.go | 4 +- xmlWorksheet.go | 2 +- 16 files changed, 209 insertions(+), 317 deletions(-) diff --git a/calc.go b/calc.go index 402c7deeb1..8e37bb97d0 100644 --- a/calc.go +++ b/calc.go @@ -380,17 +380,17 @@ type formulaFuncs struct { // BESSELJ // BESSELK // BESSELY -// BETADIST // BETA.DIST -// BETAINV // BETA.INV +// BETADIST +// BETAINV // BIN2DEC // BIN2HEX // BIN2OCT -// BINOMDIST // BINOM.DIST // BINOM.DIST.RANGE // BINOM.INV +// BINOMDIST // BITAND // BITLSHIFT // BITOR @@ -402,12 +402,12 @@ type formulaFuncs struct { // CHAR // CHIDIST // CHIINV -// CHITEST // CHISQ.DIST // CHISQ.DIST.RT // CHISQ.INV // CHISQ.INV.RT // CHISQ.TEST +// CHITEST // CHOOSE // CLEAN // CODE @@ -477,8 +477,8 @@ type formulaFuncs struct { // DURATION // DVAR // DVARP -// EFFECT // EDATE +// EFFECT // ENCODEURL // EOMONTH // ERF @@ -492,16 +492,17 @@ type formulaFuncs struct { // EXP // EXPON.DIST // EXPONDIST +// F.DIST +// F.DIST.RT +// F.INV +// F.INV.RT +// F.TEST // FACT // FACTDOUBLE // FALSE -// F.DIST -// F.DIST.RT // FDIST // FIND // FINDB -// F.INV -// F.INV.RT // FINV // FISHER // FISHERINV @@ -510,14 +511,13 @@ type formulaFuncs struct { // FLOOR.MATH // FLOOR.PRECISE // FORMULATEXT -// F.TEST // FTEST // FV // FVSCHEDULE // GAMMA // GAMMA.DIST -// GAMMADIST // GAMMA.INV +// GAMMADIST // GAMMAINV // GAMMALN // GAMMALN.PRECISE @@ -579,12 +579,12 @@ type formulaFuncs struct { // ISNA // ISNONTEXT // ISNUMBER -// ISODD -// ISREF -// ISTEXT // ISO.CEILING +// ISODD // ISOWEEKNUM // ISPMT +// ISREF +// ISTEXT // KURT // LARGE // LCM @@ -597,8 +597,8 @@ type formulaFuncs struct { // LOG10 // LOGINV // LOGNORM.DIST -// LOGNORMDIST // LOGNORM.INV +// LOGNORMDIST // LOOKUP // LOWER // MATCH @@ -633,12 +633,12 @@ type formulaFuncs struct { // NETWORKDAYS.INTL // NOMINAL // NORM.DIST -// NORMDIST // NORM.INV -// NORMINV // NORM.S.DIST -// NORMSDIST // NORM.S.INV +// NORMDIST +// NORMINV +// NORMSDIST // NORMSINV // NOT // NOW @@ -652,19 +652,19 @@ type formulaFuncs struct { // OR // PDURATION // PEARSON +// PERCENTILE // PERCENTILE.EXC // PERCENTILE.INC -// PERCENTILE +// PERCENTRANK // PERCENTRANK.EXC // PERCENTRANK.INC -// PERCENTRANK // PERMUT // PERMUTATIONA // PHI // PI // PMT -// POISSON.DIST // POISSON +// POISSON.DIST // POWER // PPMT // PRICE @@ -734,20 +734,21 @@ type formulaFuncs struct { // SWITCH // SYD // T +// T.DIST +// T.DIST.2T +// T.DIST.RT +// T.INV +// T.INV.2T +// T.TEST // TAN // TANH // TBILLEQ // TBILLPRICE // TBILLYIELD -// T.DIST -// T.DIST.2T -// T.DIST.RT // TDIST // TEXTJOIN // TIME // TIMEVALUE -// T.INV -// T.INV.2T // TINV // TODAY // TRANSPOSE @@ -756,7 +757,6 @@ type formulaFuncs struct { // TRIMMEAN // TRUE // TRUNC -// T.TEST // TTEST // TYPE // UNICHAR @@ -14162,17 +14162,23 @@ func (fn *formulaFuncs) COLUMN(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(col)) } -// calcColumnsMinMax calculation min and max value for given formula arguments -// sequence of the formula function COLUMNS. -func calcColumnsMinMax(argsList *list.List) (min, max int) { +// calcColsRowsMinMax calculation min and max value for given formula arguments +// sequence of the formula functions COLUMNS and ROWS. +func calcColsRowsMinMax(cols bool, argsList *list.List) (min, max int) { + getVal := func(cols bool, cell cellRef) int { + if cols { + return cell.Col + } + return cell.Row + } if argsList.Front().Value.(formulaArg).cellRanges != nil && argsList.Front().Value.(formulaArg).cellRanges.Len() > 0 { crs := argsList.Front().Value.(formulaArg).cellRanges for cr := crs.Front(); cr != nil; cr = cr.Next() { if min == 0 { - min = cr.Value.(cellRange).From.Col + min = getVal(cols, cr.Value.(cellRange).From) } - if max < cr.Value.(cellRange).To.Col { - max = cr.Value.(cellRange).To.Col + if max < getVal(cols, cr.Value.(cellRange).To) { + max = getVal(cols, cr.Value.(cellRange).To) } } } @@ -14180,10 +14186,10 @@ func calcColumnsMinMax(argsList *list.List) (min, max int) { cr := argsList.Front().Value.(formulaArg).cellRefs for refs := cr.Front(); refs != nil; refs = refs.Next() { if min == 0 { - min = refs.Value.(cellRef).Col + min = getVal(cols, refs.Value.(cellRef)) } - if max < refs.Value.(cellRef).Col { - max = refs.Value.(cellRef).Col + if max < getVal(cols, refs.Value.(cellRef)) { + max = getVal(cols, refs.Value.(cellRef)) } } } @@ -14198,7 +14204,7 @@ func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COLUMNS requires 1 argument") } - min, max := calcColumnsMinMax(argsList) + min, max := calcColsRowsMinMax(true, argsList) if max == MaxColumns { return newNumberFormulaArg(float64(MaxColumns)) } @@ -14411,8 +14417,8 @@ func (fn *formulaFuncs) TRANSPOSE(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "TRANSPOSE requires 1 argument") } args := argsList.Back().Value.(formulaArg).ToList() - rmin, rmax := calcRowsMinMax(argsList) - cmin, cmax := calcColumnsMinMax(argsList) + rmin, rmax := calcColsRowsMinMax(false, argsList) + cmin, cmax := calcColsRowsMinMax(true, argsList) cols, rows := cmax-cmin+1, rmax-rmin+1 src := make([][]formulaArg, 0) for i := 0; i < len(args); i += cols { @@ -14931,34 +14937,6 @@ func (fn *formulaFuncs) ROW(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(row)) } -// calcRowsMinMax calculation min and max value for given formula arguments -// sequence of the formula function ROWS. -func calcRowsMinMax(argsList *list.List) (min, max int) { - if argsList.Front().Value.(formulaArg).cellRanges != nil && argsList.Front().Value.(formulaArg).cellRanges.Len() > 0 { - crs := argsList.Front().Value.(formulaArg).cellRanges - for cr := crs.Front(); cr != nil; cr = cr.Next() { - if min == 0 { - min = cr.Value.(cellRange).From.Row - } - if max < cr.Value.(cellRange).To.Row { - max = cr.Value.(cellRange).To.Row - } - } - } - if argsList.Front().Value.(formulaArg).cellRefs != nil && argsList.Front().Value.(formulaArg).cellRefs.Len() > 0 { - cr := argsList.Front().Value.(formulaArg).cellRefs - for refs := cr.Front(); refs != nil; refs = refs.Next() { - if min == 0 { - min = refs.Value.(cellRef).Row - } - if max < refs.Value.(cellRef).Row { - max = refs.Value.(cellRef).Row - } - } - } - return -} - // ROWS function takes an Excel range and returns the number of rows that are // contained within the range. The syntax of the function is: // @@ -14967,7 +14945,7 @@ func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ROWS requires 1 argument") } - min, max := calcRowsMinMax(argsList) + min, max := calcColsRowsMinMax(false, argsList) if max == TotalRows { return newStringFormulaArg(strconv.Itoa(TotalRows)) } @@ -15649,35 +15627,35 @@ func (fn *formulaFuncs) prepareDataValueArgs(n int, argsList *list.List) formula return newListFormulaArg(dataValues) } -// DISC function calculates the Discount Rate for a security. The syntax of -// the function is: -// -// DISC(settlement,maturity,pr,redemption,[basis]) -func (fn *formulaFuncs) DISC(argsList *list.List) formulaArg { +// discIntrate is an implementation of the formula functions DISC and INTRATE. +func (fn *formulaFuncs) discIntrate(name string, argsList *list.List) formulaArg { if argsList.Len() != 4 && argsList.Len() != 5 { - return newErrorFormulaArg(formulaErrorVALUE, "DISC requires 4 or 5 arguments") + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 4 or 5 arguments", name)) } args := fn.prepareDataValueArgs(2, argsList) if args.Type != ArgList { return args } - settlement, maturity := args.List[0], args.List[1] + settlement, maturity, argName := args.List[0], args.List[1], "pr" if maturity.Number <= settlement.Number { - return newErrorFormulaArg(formulaErrorNUM, "DISC requires maturity > settlement") + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > settlement", name)) } - pr := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() - if pr.Type != ArgNumber { + prInvestment := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if prInvestment.Type != ArgNumber { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - if pr.Number <= 0 { - return newErrorFormulaArg(formulaErrorNUM, "DISC requires pr > 0") + if prInvestment.Number <= 0 { + if name == "INTRATE" { + argName = "investment" + } + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires %s > 0", name, argName)) } redemption := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() if redemption.Type != ArgNumber { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } if redemption.Number <= 0 { - return newErrorFormulaArg(formulaErrorNUM, "DISC requires redemption > 0") + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires redemption > 0", name)) } basis := newNumberFormulaArg(0) if argsList.Len() == 5 { @@ -15689,7 +15667,18 @@ func (fn *formulaFuncs) DISC(argsList *list.List) formulaArg { if frac.Type != ArgNumber { return frac } - return newNumberFormulaArg((redemption.Number - pr.Number) / redemption.Number / frac.Number) + if name == "INTRATE" { + return newNumberFormulaArg((redemption.Number - prInvestment.Number) / prInvestment.Number / frac.Number) + } + return newNumberFormulaArg((redemption.Number - prInvestment.Number) / redemption.Number / frac.Number) +} + +// DISC function calculates the Discount Rate for a security. The syntax of +// the function is: +// +// DISC(settlement,maturity,pr,redemption,[basis]) +func (fn *formulaFuncs) DISC(argsList *list.List) formulaArg { + return fn.discIntrate("DISC", argsList) } // DOLLARDE function converts a dollar value in fractional notation, into a @@ -16007,42 +15996,7 @@ func (fn *formulaFuncs) FVSCHEDULE(argsList *list.List) formulaArg { // // INTRATE(settlement,maturity,investment,redemption,[basis]) func (fn *formulaFuncs) INTRATE(argsList *list.List) formulaArg { - if argsList.Len() != 4 && argsList.Len() != 5 { - return newErrorFormulaArg(formulaErrorVALUE, "INTRATE requires 4 or 5 arguments") - } - args := fn.prepareDataValueArgs(2, argsList) - if args.Type != ArgList { - return args - } - settlement, maturity := args.List[0], args.List[1] - if maturity.Number <= settlement.Number { - return newErrorFormulaArg(formulaErrorNUM, "INTRATE requires maturity > settlement") - } - investment := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() - if investment.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - if investment.Number <= 0 { - return newErrorFormulaArg(formulaErrorNUM, "INTRATE requires investment > 0") - } - redemption := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() - if redemption.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - if redemption.Number <= 0 { - return newErrorFormulaArg(formulaErrorNUM, "INTRATE requires redemption > 0") - } - basis := newNumberFormulaArg(0) - if argsList.Len() == 5 { - if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) - } - } - frac := yearFrac(settlement.Number, maturity.Number, int(basis.Number)) - if frac.Type != ArgNumber { - return frac - } - return newNumberFormulaArg((redemption.Number - investment.Number) / investment.Number / frac.Number) + return fn.discIntrate("INTRATE", argsList) } // IPMT function calculates the interest payment, during a specific period of a @@ -16756,54 +16710,81 @@ func (fn *formulaFuncs) price(settlement, maturity, rate, yld, redemption, frequ return newNumberFormulaArg(ret) } -// PRICE function calculates the price, per $100 face value of a security that -// pays periodic interest. The syntax of the function is: -// -// PRICE(settlement,maturity,rate,yld,redemption,frequency,[basis]) -func (fn *formulaFuncs) PRICE(argsList *list.List) formulaArg { - if argsList.Len() != 6 && argsList.Len() != 7 { - return newErrorFormulaArg(formulaErrorVALUE, "PRICE requires 6 or 7 arguments") - } - args := fn.prepareDataValueArgs(2, argsList) - if args.Type != ArgList { - return args - } - settlement, maturity := args.List[0], args.List[1] - rate := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() +// checkPriceYieldArgs checking and prepare arguments for the formula functions +// PRICE and YIELD. +func checkPriceYieldArgs(name string, rate, prYld, redemption, frequency formulaArg) formulaArg { if rate.Type != ArgNumber { return rate } if rate.Number < 0 { - return newErrorFormulaArg(formulaErrorNUM, "PRICE requires rate >= 0") + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires rate >= 0", name)) } - yld := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() - if yld.Type != ArgNumber { - return yld - } - if yld.Number < 0 { - return newErrorFormulaArg(formulaErrorNUM, "PRICE requires yld >= 0") + if prYld.Type != ArgNumber { + return prYld } - redemption := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber() if redemption.Type != ArgNumber { return redemption } - if redemption.Number <= 0 { - return newErrorFormulaArg(formulaErrorNUM, "PRICE requires redemption > 0") + if name == "PRICE" { + if prYld.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "PRICE requires yld >= 0") + } + if redemption.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "PRICE requires redemption > 0") + } + } + if name == "YIELD" { + if prYld.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "YIELD requires pr > 0") + } + if redemption.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "YIELD requires redemption >= 0") + } } - frequency := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() if frequency.Type != ArgNumber { return frequency } if !validateFrequency(frequency.Number) { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } + return newEmptyFormulaArg() +} + +// priceYield is an implementation of the formula functions PRICE and YIELD. +func (fn *formulaFuncs) priceYield(name string, argsList *list.List) formulaArg { + if argsList.Len() != 6 && argsList.Len() != 7 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 6 or 7 arguments", name)) + } + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args + } + settlement, maturity := args.List[0], args.List[1] + rate := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + prYld := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + redemption := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + frequency := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + if arg := checkPriceYieldArgs(name, rate, prYld, redemption, frequency); arg.Type != ArgEmpty { + return arg + } basis := newNumberFormulaArg(0) if argsList.Len() == 7 { if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } } - return fn.price(settlement, maturity, rate, yld, redemption, frequency, basis) + if name == "PRICE" { + return fn.price(settlement, maturity, rate, prYld, redemption, frequency, basis) + } + return fn.yield(settlement, maturity, rate, prYld, redemption, frequency, basis) +} + +// PRICE function calculates the price, per $100 face value of a security that +// pays periodic interest. The syntax of the function is: +// +// PRICE(settlement,maturity,rate,yld,redemption,frequency,[basis]) +func (fn *formulaFuncs) PRICE(argsList *list.List) formulaArg { + return fn.priceYield("PRICE", argsList) } // PRICEDISC function calculates the price, per $100 face value of a @@ -17535,49 +17516,7 @@ func (fn *formulaFuncs) yield(settlement, maturity, rate, pr, redemption, freque // // YIELD(settlement,maturity,rate,pr,redemption,frequency,[basis]) func (fn *formulaFuncs) YIELD(argsList *list.List) formulaArg { - if argsList.Len() != 6 && argsList.Len() != 7 { - return newErrorFormulaArg(formulaErrorVALUE, "YIELD requires 6 or 7 arguments") - } - args := fn.prepareDataValueArgs(2, argsList) - if args.Type != ArgList { - return args - } - settlement, maturity := args.List[0], args.List[1] - rate := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() - if rate.Type != ArgNumber { - return rate - } - if rate.Number < 0 { - return newErrorFormulaArg(formulaErrorNUM, "PRICE requires rate >= 0") - } - pr := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() - if pr.Type != ArgNumber { - return pr - } - if pr.Number <= 0 { - return newErrorFormulaArg(formulaErrorNUM, "PRICE requires pr > 0") - } - redemption := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber() - if redemption.Type != ArgNumber { - return redemption - } - if redemption.Number < 0 { - return newErrorFormulaArg(formulaErrorNUM, "PRICE requires redemption >= 0") - } - frequency := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() - if frequency.Type != ArgNumber { - return frequency - } - if !validateFrequency(frequency.Number) { - return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) - } - basis := newNumberFormulaArg(0) - if argsList.Len() == 7 { - if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) - } - } - return fn.yield(settlement, maturity, rate, pr, redemption, frequency, basis) + return fn.priceYield("YIELD", argsList) } // YIELDDISC function calculates the annual yield of a discounted security. diff --git a/calc_test.go b/calc_test.go index b9c9a8d87b..a706f3de8f 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4334,9 +4334,9 @@ func TestCalcCellValue(t *testing.T) { "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4,\"\")": {"#NUM!", "#NUM!"}, "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,3)": {"#NUM!", "#NUM!"}, "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4,5)": {"#NUM!", "invalid basis"}, - "=YIELD(\"01/01/2010\",\"06/30/2015\",-1,101,100,4)": {"#NUM!", "PRICE requires rate >= 0"}, - "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,0,100,4)": {"#NUM!", "PRICE requires pr > 0"}, - "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,-1,4)": {"#NUM!", "PRICE requires redemption >= 0"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",-1,101,100,4)": {"#NUM!", "YIELD requires rate >= 0"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,0,100,4)": {"#NUM!", "YIELD requires pr > 0"}, + "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,-1,4)": {"#NUM!", "YIELD requires redemption >= 0"}, // YIELDDISC "=YIELDDISC()": {"#VALUE!", "YIELDDISC requires 4 or 5 arguments"}, "=YIELDDISC(\"\",\"06/30/2017\",97,100,0)": {"#VALUE!", "#VALUE!"}, diff --git a/date.go b/date.go index 94ce218371..a59c694705 100644 --- a/date.go +++ b/date.go @@ -114,7 +114,7 @@ func julianDateToGregorianTime(part1, part2 float64) time.Time { // "Communications of the ACM" in 1968 (published in CACM, volume 11, number // 10, October 1968, p.657). None of those programmers seems to have found it // necessary to explain the constants or variable names set out by Henry F. -// Fliegel and Thomas C. Van Flandern. Maybe one day I'll buy that jounal and +// Fliegel and Thomas C. Van Flandern. Maybe one day I'll buy that journal and // expand an explanation here - that day is not today. func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) { l := jd + 68569 @@ -163,7 +163,7 @@ func timeFromExcelTime(excelTime float64, date1904 bool) time.Time { return date.Truncate(time.Second) } -// ExcelDateToTime converts a float-based excel date representation to a time.Time. +// ExcelDateToTime converts a float-based Excel date representation to a time.Time. func ExcelDateToTime(excelDate float64, use1904Format bool) (time.Time, error) { if excelDate < 0 { return time.Time{}, newInvalidExcelDateError(excelDate) diff --git a/errors.go b/errors.go index 1a6cc8a07e..96eed6fc5e 100644 --- a/errors.go +++ b/errors.go @@ -143,7 +143,7 @@ var ( ErrWorkbookFileFormat = errors.New("unsupported workbook file format") // ErrMaxFilePathLength defined the error message on receive the file path // length overflow. - ErrMaxFilePathLength = errors.New("file path length exceeds maximum limit") + ErrMaxFilePathLength = fmt.Errorf("file path length exceeds maximum limit %d characters", MaxFilePathLength) // ErrUnknownEncryptMechanism defined the error message on unsupported // encryption mechanism. ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism") diff --git a/excelize.go b/excelize.go index d677285644..a0eaecae11 100644 --- a/excelize.go +++ b/excelize.go @@ -60,7 +60,7 @@ type File struct { // the spreadsheet from non-UTF-8 encoding. type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) -// Options define the options for o`pen and reading spreadsheet. +// Options define the options for opening and reading the spreadsheet. // // MaxCalcIterations specifies the maximum iterations for iterative // calculation, the default value is 0. @@ -70,7 +70,7 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e // RawCellValue specifies if apply the number format for the cell value or get // the raw value. // -// UnzipSizeLimit specifies the unzip size limit in bytes on open the +// UnzipSizeLimit specifies to unzip size limit in bytes on open the // spreadsheet, this value should be greater than or equal to // UnzipXMLSizeLimit, the default size limit is 16GB. // @@ -106,7 +106,7 @@ type Options struct { CultureInfo CultureName } -// OpenFile take the name of an spreadsheet file and returns a populated +// OpenFile take the name of a spreadsheet file and returns a populated // spreadsheet file struct for it. For example, open spreadsheet with // password protection: // diff --git a/hsl.go b/hsl.go index c30c165a5b..68ddf21704 100644 --- a/hsl.go +++ b/hsl.go @@ -60,7 +60,7 @@ func hslModel(c color.Color) color.Color { return HSL{h, s, l} } -// RGBToHSL converts an RGB triple to a HSL triple. +// RGBToHSL converts an RGB triple to an HSL triple. func RGBToHSL(r, g, b uint8) (h, s, l float64) { fR := float64(r) / 255 fG := float64(g) / 255 @@ -95,7 +95,7 @@ func RGBToHSL(r, g, b uint8) (h, s, l float64) { return } -// HSLToRGB converts an HSL triple to a RGB triple. +// HSLToRGB converts an HSL triple to an RGB triple. func HSLToRGB(h, s, l float64) (r, g, b uint8) { var fR, fG, fB float64 if s == 0 { diff --git a/numfmt.go b/numfmt.go index 9e48a6e5ed..f39ad611e5 100644 --- a/numfmt.go +++ b/numfmt.go @@ -1136,7 +1136,7 @@ func getNumberPartLen(n float64) (int, int) { return len(parts[0]), 0 } -// getNumberFmtConf generate the number format padding and place holder +// getNumberFmtConf generate the number format padding and placeholder // configurations. func (nf *numberFormat) getNumberFmtConf() { for _, token := range nf.section[nf.sectionIdx].Items { @@ -1183,9 +1183,9 @@ func (nf *numberFormat) printNumberLiteral(text string) string { if nf.usePositive { result += "-" } - for i, token := range nf.section[nf.sectionIdx].Items { + for _, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { - if err, changeNumFmtCode := nf.currencyLanguageHandler(i, token); err != nil || changeNumFmtCode { + if err, changeNumFmtCode := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { return nf.value } result += nf.currencyString @@ -1321,7 +1321,7 @@ func (nf *numberFormat) dateTimeHandler() string { nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false for i, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { - if err, changeNumFmtCode := nf.currencyLanguageHandler(i, token); err != nil || changeNumFmtCode { + if err, changeNumFmtCode := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { return nf.value } nf.result += nf.currencyString @@ -1392,7 +1392,7 @@ func (nf *numberFormat) positiveHandler() string { // currencyLanguageHandler will be handling currency and language types tokens // for a number format expression. -func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (error, bool) { +func (nf *numberFormat) currencyLanguageHandler(token nfp.Token) (error, bool) { for _, part := range token.Parts { if inStrSlice(supportedTokenTypes, part.Token.TType, true) == -1 { return ErrUnsupportedNumberFormat, false @@ -1491,7 +1491,7 @@ func localMonthsNameFrench(t time.Time, abbr int) string { // localMonthsNameIrish returns the Irish name of the month. func localMonthsNameIrish(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesIrishAbbr[int(t.Month()-1)] + return monthNamesIrishAbbr[(t.Month() - 1)] } if abbr == 4 { return monthNamesIrish[int(t.Month())-1] @@ -1524,7 +1524,7 @@ func localMonthsNameGerman(t time.Time, abbr int) string { // localMonthsNameChinese1 returns the Chinese name of the month. func localMonthsNameChinese1(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesChineseAbbrPlus[int(t.Month())] + return monthNamesChineseAbbrPlus[t.Month()] } if abbr == 4 { return monthNamesChinesePlus[int(t.Month())-1] @@ -1543,7 +1543,7 @@ func localMonthsNameChinese2(t time.Time, abbr int) string { // localMonthsNameChinese3 returns the Chinese name of the month. func localMonthsNameChinese3(t time.Time, abbr int) string { if abbr == 3 || abbr == 4 { - return monthNamesChineseAbbrPlus[int(t.Month())] + return monthNamesChineseAbbrPlus[t.Month()] } return strconv.Itoa(int(t.Month())) } @@ -1551,7 +1551,7 @@ func localMonthsNameChinese3(t time.Time, abbr int) string { // localMonthsNameKorean returns the Korean name of the month. func localMonthsNameKorean(t time.Time, abbr int) string { if abbr == 3 || abbr == 4 { - return monthNamesKoreanAbbrPlus[int(t.Month())] + return monthNamesKoreanAbbrPlus[t.Month()] } return strconv.Itoa(int(t.Month())) } @@ -1562,7 +1562,7 @@ func localMonthsNameTraditionalMongolian(t time.Time, abbr int) string { if abbr == 5 { return "M" } - return monthNamesTradMongolian[int(t.Month()-1)] + return monthNamesTradMongolian[t.Month()-1] } // localMonthsNameRussian returns the Russian name of the month. @@ -1642,12 +1642,12 @@ func localMonthsNameWelsh(t time.Time, abbr int) string { // localMonthsNameVietnamese returns the Vietnamese name of the month. func localMonthsNameVietnamese(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesVietnameseAbbr3[int(t.Month()-1)] + return monthNamesVietnameseAbbr3[t.Month()-1] } if abbr == 5 { - return monthNamesVietnameseAbbr5[int(t.Month()-1)] + return monthNamesVietnameseAbbr5[t.Month()-1] } - return monthNamesVietnamese[int(t.Month()-1)] + return monthNamesVietnamese[t.Month()-1] } // localMonthsNameWolof returns the Wolof name of the month. @@ -1675,7 +1675,7 @@ func localMonthsNameXhosa(t time.Time, abbr int) string { // localMonthsNameYi returns the Yi name of the month. func localMonthsNameYi(t time.Time, abbr int) string { if abbr == 3 || abbr == 4 { - return monthNamesYiSuffix[int(t.Month()-1)] + return monthNamesYiSuffix[t.Month()-1] } return string([]rune(monthNamesYi[int(t.Month())-1])[:1]) } @@ -1683,7 +1683,7 @@ func localMonthsNameYi(t time.Time, abbr int) string { // localMonthsNameZulu returns the Zulu name of the month. func localMonthsNameZulu(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesZuluAbbr[int(t.Month()-1)] + return monthNamesZuluAbbr[t.Month()-1] } if abbr == 4 { return monthNamesZulu[int(t.Month())-1] @@ -1737,8 +1737,8 @@ func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) { return } } - nf.yearsHandler(i, token) - nf.daysHandler(i, token) + nf.yearsHandler(token) + nf.daysHandler(token) nf.hoursHandler(i, token) nf.minutesHandler(token) nf.secondsHandler(token) @@ -1746,7 +1746,7 @@ func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) { // yearsHandler will be handling years in the date and times types tokens for a // number format expression. -func (nf *numberFormat) yearsHandler(i int, token nfp.Token) { +func (nf *numberFormat) yearsHandler(token nfp.Token) { years := strings.Contains(strings.ToUpper(token.TValue), "Y") if years && len(token.TValue) <= 2 { nf.result += strconv.Itoa(nf.t.Year())[2:] @@ -1760,7 +1760,7 @@ func (nf *numberFormat) yearsHandler(i int, token nfp.Token) { // daysHandler will be handling days in the date and times types tokens for a // number format expression. -func (nf *numberFormat) daysHandler(i int, token nfp.Token) { +func (nf *numberFormat) daysHandler(token nfp.Token) { if strings.Contains(strings.ToUpper(token.TValue), "D") { switch len(token.TValue) { case 1: diff --git a/numfmt_test.go b/numfmt_test.go index c41fd9408d..c49393f661 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -1093,7 +1093,7 @@ func TestNumFmt(t *testing.T) { } } nf := numberFormat{} - err, changeNumFmtCode := nf.currencyLanguageHandler(0, nfp.Token{Parts: []nfp.Part{{}}}) + err, changeNumFmtCode := nf.currencyLanguageHandler(nfp.Token{Parts: []nfp.Part{{}}}) assert.Equal(t, ErrUnsupportedNumberFormat, err) assert.False(t, changeNumFmtCode) } diff --git a/rows.go b/rows.go index 60b74b3a90..7351d160c4 100644 --- a/rows.go +++ b/rows.go @@ -79,7 +79,7 @@ type Rows struct { curRowOpts, seekRowOpts RowOpts } -// Next will return true if find the next row element. +// Next will return true if it finds the next row element. func (rows *Rows) Next() bool { rows.seekRow++ if rows.curRow >= rows.seekRow { @@ -297,7 +297,9 @@ func (f *File) getFromStringItem(index int) string { } needClose, decoder, tempFile, err := f.xmlDecoder(defaultXMLPathSharedStrings) if needClose && err == nil { - defer tempFile.Close() + defer func() { + err = tempFile.Close() + }() } f.sharedStringItem = [][]uint{} f.sharedStringTemp, _ = os.CreateTemp(os.TempDir(), "excelize-") diff --git a/rows_test.go b/rows_test.go index f94adbd037..acf50ff9e8 100644 --- a/rows_test.go +++ b/rows_test.go @@ -416,6 +416,23 @@ func TestInsertRowsInEmptyFile(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) } +func prepareTestBook2() (*File, error) { + f := NewFile() + for cell, val := range map[string]string{ + "A1": "A1 Value", + "A2": "A2 Value", + "A3": "A3 Value", + "B1": "B1 Value", + "B2": "B2 Value", + "B3": "B3 Value", + } { + if err := f.SetCellStr("Sheet1", cell, val); err != nil { + return f, err + } + } + return f, nil +} + func TestDuplicateRowFromSingleRow(t *testing.T) { const sheet = "Sheet1" outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") @@ -512,7 +529,6 @@ func TestDuplicateRowUpdateDuplicatedRows(t *testing.T) { func TestDuplicateRowFirstOfMultipleRows(t *testing.T) { const sheet = "Sheet1" outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") - cells := map[string]string{ "A1": "A1 Value", "A2": "A2 Value", @@ -521,18 +537,9 @@ func TestDuplicateRowFirstOfMultipleRows(t *testing.T) { "B2": "B2 Value", "B3": "B3 Value", } - - newFileWithDefaults := func() *File { - f := NewFile() - for cell, val := range cells { - assert.NoError(t, f.SetCellStr(sheet, cell, val)) - } - return f - } - t.Run("FirstOfMultipleRows", func(t *testing.T) { - f := newFileWithDefaults() - + f, err := prepareTestBook2() + assert.NoError(t, err) assert.NoError(t, f.DuplicateRow(sheet, 1)) if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "FirstOfMultipleRows"))) { @@ -635,18 +642,9 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) { "B2": "B2 Value", "B3": "B3 Value", } - - newFileWithDefaults := func() *File { - f := NewFile() - for cell, val := range cells { - assert.NoError(t, f.SetCellStr(sheet, cell, val)) - } - return f - } - t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) { - f := newFileWithDefaults() - + f, err := prepareTestBook2() + assert.NoError(t, err) assert.NoError(t, f.DuplicateRowTo(sheet, 1, 3)) if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToMiddleOfData"))) { @@ -671,7 +669,6 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) { func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) { const sheet = "Sheet1" outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") - cells := map[string]string{ "A1": "A1 Value", "A2": "A2 Value", @@ -680,18 +677,9 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) { "B2": "B2 Value", "B3": "B3 Value", } - - newFileWithDefaults := func() *File { - f := NewFile() - for cell, val := range cells { - assert.NoError(t, f.SetCellStr(sheet, cell, val)) - } - return f - } - t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) { - f := newFileWithDefaults() - + f, err := prepareTestBook2() + assert.NoError(t, err) assert.NoError(t, f.DuplicateRowTo(sheet, 1, 7)) if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToEmptyRows"))) { @@ -716,7 +704,6 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) { func TestDuplicateRowInsertBefore(t *testing.T) { const sheet = "Sheet1" outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") - cells := map[string]string{ "A1": "A1 Value", "A2": "A2 Value", @@ -725,18 +712,9 @@ func TestDuplicateRowInsertBefore(t *testing.T) { "B2": "B2 Value", "B3": "B3 Value", } - - newFileWithDefaults := func() *File { - f := NewFile() - for cell, val := range cells { - assert.NoError(t, f.SetCellStr(sheet, cell, val)) - } - return f - } - t.Run("InsertBefore", func(t *testing.T) { - f := newFileWithDefaults() - + f, err := prepareTestBook2() + assert.NoError(t, err) assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1)) assert.NoError(t, f.DuplicateRowTo(sheet, 10, 4)) @@ -763,7 +741,6 @@ func TestDuplicateRowInsertBefore(t *testing.T) { func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) { const sheet = "Sheet1" outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") - cells := map[string]string{ "A1": "A1 Value", "A2": "A2 Value", @@ -772,18 +749,9 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) { "B2": "B2 Value", "B3": "B3 Value", } - - newFileWithDefaults := func() *File { - f := NewFile() - for cell, val := range cells { - assert.NoError(t, f.SetCellStr(sheet, cell, val)) - } - return f - } - t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { - f := newFileWithDefaults() - + f, err := prepareTestBook2() + assert.NoError(t, err) assert.NoError(t, f.DuplicateRowTo(sheet, 3, 1)) if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "InsertBeforeWithLargeOffset"))) { @@ -809,28 +777,11 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) { func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) { const sheet = "Sheet1" outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") - - cells := map[string]string{ - "A1": "A1 Value", - "A2": "A2 Value", - "A3": "A3 Value", - "B1": "B1 Value", - "B2": "B2 Value", - "B3": "B3 Value", - } - - newFileWithDefaults := func() *File { - f := NewFile() - for cell, val := range cells { - assert.NoError(t, f.SetCellStr(sheet, cell, val)) - } + t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { + f, err := prepareTestBook2() + assert.NoError(t, err) assert.NoError(t, f.MergeCell(sheet, "B2", "C2")) assert.NoError(t, f.MergeCell(sheet, "C6", "C8")) - return f - } - - t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { - f := newFileWithDefaults() assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1)) assert.NoError(t, f.DuplicateRowTo(sheet, 1, 8)) diff --git a/styles.go b/styles.go index 9dc142e5d5..70c11d596b 100644 --- a/styles.go +++ b/styles.go @@ -1309,7 +1309,7 @@ func newNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { if !ok { fc, currency := currencyNumFmt[style.NumFmt] if !currency { - return setLangNumFmt(styleSheet, style) + return setLangNumFmt(style) } fc = strings.ReplaceAll(fc, "0.00", dp) if style.NegRed { @@ -1375,7 +1375,7 @@ func getCustomNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (customNumFmtID } // setLangNumFmt provides a function to set number format code with language. -func setLangNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { +func setLangNumFmt(style *Style) int { if (27 <= style.NumFmt && style.NumFmt <= 36) || (50 <= style.NumFmt && style.NumFmt <= 81) { return style.NumFmt } @@ -1585,7 +1585,7 @@ func newBorders(style *Style) *xlsxBorder { return &border } -// setCellXfs provides a function to set describes all of the formatting for a +// setCellXfs provides a function to set describes all the formatting for a // cell. func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, applyAlignment, applyProtection bool, alignment *xlsxAlignment, protection *xlsxProtection) (int, error) { var xf xlsxXf @@ -2451,7 +2451,7 @@ func extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatO if ext.URI == ExtURIConditionalFormattings { decodeCondFmts := new(decodeX14ConditionalFormattings) if err := xml.Unmarshal([]byte(ext.Content), &decodeCondFmts); err == nil { - condFmts := []decodeX14ConditionalFormatting{} + var condFmts []decodeX14ConditionalFormatting if err = xml.Unmarshal([]byte(decodeCondFmts.Content), &condFmts); err == nil { extractDataBarRule(condFmts) } diff --git a/table.go b/table.go index 094f765927..b63fe276a4 100644 --- a/table.go +++ b/table.go @@ -320,7 +320,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab // x == *b // ends with b // x != *b // doesn't end with b // x == *b* // contains b -// x != *b* // doesn't contains b +// x != *b* // doesn't contain b // // You can also use '*' to match any character or number and '?' to match any // single character or number. No other regular expression quantifier is @@ -538,7 +538,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str } token := tokens[2] // Special handling for Blanks/NonBlanks. - re := blankFormat.MatchString((strings.ToLower(token))) + re := blankFormat.MatchString(strings.ToLower(token)) if re { // Only allow Equals or NotEqual in this context. if operator != 2 && operator != 5 { diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 612bb62ed4..c737ac08f6 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -199,7 +199,7 @@ type decodeSpPr struct { // decodePic elements encompass the definition of pictures within the // DrawingML framework. While pictures are in many ways very similar to shapes // they have specific properties that are unique in order to optimize for -// picture- specific scenarios. +// picture-specific scenarios. type decodePic struct { NvPicPr decodeNvPicPr `xml:"nvPicPr"` BlipFill decodeBlipFill `xml:"blipFill"` diff --git a/xmlStyles.go b/xmlStyles.go index 9700919aa9..74b9119b16 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -65,10 +65,10 @@ type xlsxLine struct { // xlsxColor is a common mapping used for both the fgColor and bgColor elements. // Foreground color of the cell fill pattern. Cell fill patterns operate with -// two colors: a background color and a foreground color. These combine together +// two colors: a background color and a foreground color. These combine // to make a patterned cell fill. Background color of the cell fill pattern. // Cell fill patterns operate with two colors: a background color and a -// foreground color. These combine together to make a patterned cell fill. +// foreground color. These combine to make a patterned cell fill. type xlsxColor struct { Auto bool `xml:"auto,attr,omitempty"` RGB string `xml:"rgb,attr,omitempty"` @@ -103,7 +103,7 @@ type xlsxFont struct { Scheme *attrValString `xml:"scheme"` } -// xlsxFills directly maps the fills element. This element defines the cell +// xlsxFills directly maps the fills' element. This element defines the cell // fills portion of the Styles part, consisting of a sequence of fill records. A // cell fill consists of a background color, foreground color, and pattern to be // applied across the cell. @@ -147,7 +147,7 @@ type xlsxGradientFillStop struct { Color xlsxColor `xml:"color,omitempty"` } -// xlsxBorders directly maps the borders element. This element contains borders +// xlsxBorders directly maps the borders' element. This element contains borders // formatting information, specifying all border definitions for all cells in // the workbook. type xlsxBorders struct { @@ -205,7 +205,7 @@ type xlsxCellStyleXfs struct { Xf []xlsxXf `xml:"xf,omitempty"` } -// xlsxXf directly maps the xf element. A single xf element describes all of the +// xlsxXf directly maps the xf element. A single xf element describes all the // formatting for a cell. type xlsxXf struct { NumFmtID *int `xml:"numFmtId,attr"` @@ -236,8 +236,8 @@ type xlsxCellXfs struct { } // xlsxDxfs directly maps the dxfs element. This element contains the master -// differential formatting records (dxf's) which define formatting for all non- -// cell formatting in this workbook. Whereas xf records fully specify a +// differential formatting records (dxf's) which define formatting for all +// non-cell formatting in this workbook. Whereas xf records fully specify a // particular aspect of formatting (e.g., cell borders) by referencing those // formatting definitions elsewhere in the Styles part, dxf records specify // incremental (or differential) aspects of formatting directly inline within @@ -304,7 +304,7 @@ type xlsxNumFmt struct { FormatCode string `xml:"formatCode,attr,omitempty"` } -// xlsxStyleColors directly maps the colors element. Color information +// xlsxStyleColors directly maps the colors' element. Color information // associated with this stylesheet. This collection is written whenever the // legacy color palette has been modified (backwards compatibility settings) or // a custom color has been selected while using this workbook. diff --git a/xmlWorkbook.go b/xmlWorkbook.go index bc71bd4c9e..c00637508c 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -212,7 +212,7 @@ type xlsxPivotCache struct { // document are specified in the markup specification and can be used to store // extensions to the markup specification, whether those are future version // extensions of the markup specification or are private extensions implemented -// independently from the markup specification. Markup within an extension might +// independently of the markup specification. Markup within an extension might // not be understood by a consumer. type xlsxExtLst struct { Ext string `xml:",innerxml"` @@ -229,7 +229,7 @@ type xlsxDefinedNames struct { // xlsxDefinedName directly maps the definedName element from the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main This element // defines a defined name within this workbook. A defined name is descriptive -// text that is used to represents a cell, range of cells, formula, or constant +// text that is used to represent a cell, range of cells, formula, or constant // value. For a descriptions of the attributes see https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.definedname type xlsxDefinedName struct { Comment string `xml:"comment,attr,omitempty"` diff --git a/xmlWorksheet.go b/xmlWorksheet.go index f23c4142f6..79170deca5 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -427,7 +427,7 @@ type xlsxDataValidations struct { DataValidation []*DataValidation `xml:"dataValidation"` } -// DataValidation directly maps the a single item of data validation defined +// DataValidation directly maps the single item of data validation defined // on a range of the worksheet. type DataValidation struct { AllowBlank bool `xml:"allowBlank,attr"` From 661c0eade9cd1400688faa6d251f736d3d9a1dbe Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 5 Jun 2023 00:06:27 +0800 Subject: [PATCH 203/213] Support apply built-in number format code 22 with custom short date pattern --- excelize_test.go | 10 ++++++++++ numfmt.go | 3 +++ 2 files changed, 13 insertions(+) diff --git a/excelize_test.go b/excelize_test.go index cba9552282..5ef9207be5 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -783,6 +783,16 @@ func TestSetCellStyleNumberFormat(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet2", "L33", "L33", style)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleNumberFormat.xlsx"))) + + // Test get cell value with built-in number format code 22 with custom short date pattern + f = NewFile(Options{ShortDatePattern: "yyyy-m-dd"}) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 45074.625694444447)) + style, err = f.NewStyle(&Style{NumFmt: 22}) + assert.NoError(t, err) + assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) + cellValue, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "2023-5-28 15:01", cellValue) } func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { diff --git a/numfmt.go b/numfmt.go index f39ad611e5..b75117a6ab 100644 --- a/numfmt.go +++ b/numfmt.go @@ -1073,6 +1073,9 @@ func (f *File) langNumFmtFuncZhCN(numFmtID int) string { // getBuiltInNumFmtCode convert number format index to number format code with // specified locale and language. func (f *File) getBuiltInNumFmtCode(numFmtID int) (string, bool) { + if numFmtID == 22 && f.options.ShortDatePattern != "" { + return fmt.Sprintf("%s hh:mm", f.options.ShortDatePattern), true + } if fmtCode, ok := builtInNumFmt[numFmtID]; ok { return fmtCode, true } From 78c974d855e43011a4fd9febce476bc1e80d35e4 Mon Sep 17 00:00:00 2001 From: vb6iscool <95078692+vb6iscool@users.noreply.github.com> Date: Thu, 8 Jun 2023 09:50:38 +0800 Subject: [PATCH 204/213] New function `GetPanes` for get sheet panes and view selection (#1556) - Breaking changes: rename exported type `PaneOptions` to `Selection` - Update unit tests - Upgrade dependencies package - Add internal error variables - Simplify variable declarations --- errors.go | 6 ++++++ go.mod | 4 ++-- go.sum | 12 +++++------ numfmt.go | 12 ++++++----- sheet.go | 52 +++++++++++++++++++++++++++++++++++++++++---- sheet_test.go | 56 +++++++++++++++++++++++++++++++++++-------------- sheetview.go | 6 +----- stream_test.go | 2 +- table.go | 53 +++++++++++++++++++++------------------------- xmlWorksheet.go | 6 +++--- 10 files changed, 138 insertions(+), 71 deletions(-) diff --git a/errors.go b/errors.go index 96eed6fc5e..b8d2022f72 100644 --- a/errors.go +++ b/errors.go @@ -100,6 +100,12 @@ func newViewIdxError(viewIndex int) error { return fmt.Errorf("view index %d out of range", viewIndex) } +// newUnknownFilterTokenError defined the error message on receiving a unknown +// filter operator token. +func newUnknownFilterTokenError(token string) error { + return fmt.Errorf("unknown operator: %s", token) +} + var ( // ErrStreamSetColWidth defined the error message on set column width in // stream writing mode. diff --git a/go.mod b/go.mod index 176c00a836..58a3d71555 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( github.com/stretchr/testify v1.8.0 github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 - golang.org/x/crypto v0.8.0 + golang.org/x/crypto v0.9.0 golang.org/x/image v0.5.0 - golang.org/x/net v0.9.0 + golang.org/x/net v0.10.0 golang.org/x/text v0.9.0 ) diff --git a/go.sum b/go.sum index c5062b39c6..35ed9c2945 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83/go.mod h1:WwHg+CVyzlv/TX9 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -32,8 +32,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -43,11 +43,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/numfmt.go b/numfmt.go index b75117a6ab..012325443a 100644 --- a/numfmt.go +++ b/numfmt.go @@ -1018,8 +1018,13 @@ var ( // applyBuiltInNumFmt provides a function to returns a value after formatted // with built-in number format code, or specified sort date format code. func (f *File) applyBuiltInNumFmt(c *xlsxC, fmtCode string, numFmtID int, date1904 bool, cellType CellType) string { - if numFmtID == 14 && f.options != nil && f.options.ShortDatePattern != "" { - fmtCode = f.options.ShortDatePattern + if f.options != nil && f.options.ShortDatePattern != "" { + if numFmtID == 14 { + fmtCode = f.options.ShortDatePattern + } + if numFmtID == 22 { + fmtCode = fmt.Sprintf("%s hh:mm", f.options.ShortDatePattern) + } } return format(c.V, fmtCode, date1904, cellType, f.options) } @@ -1073,9 +1078,6 @@ func (f *File) langNumFmtFuncZhCN(numFmtID int) string { // getBuiltInNumFmtCode convert number format index to number format code with // specified locale and language. func (f *File) getBuiltInNumFmtCode(numFmtID int) (string, bool) { - if numFmtID == 22 && f.options.ShortDatePattern != "" { - return fmt.Sprintf("%s hh:mm", f.options.ShortDatePattern), true - } if fmtCode, ok := builtInNumFmt[numFmtID]; ok { return fmtCode, true } diff --git a/sheet.go b/sheet.go index 8d441dd4f9..bc35aa7e2d 100644 --- a/sheet.go +++ b/sheet.go @@ -772,7 +772,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error { } } var s []*xlsxSelection - for _, p := range panes.Panes { + for _, p := range panes.Selection { s = append(s, &xlsxSelection{ ActiveCell: p.ActiveCell, Pane: p.Pane, @@ -859,7 +859,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error { // YSplit: 0, // TopLeftCell: "B1", // ActivePane: "topRight", -// Panes: []excelize.PaneOptions{ +// Selection: []excelize.Selection{ // {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, // }, // }) @@ -874,7 +874,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error { // YSplit: 9, // TopLeftCell: "A34", // ActivePane: "bottomLeft", -// Panes: []excelize.PaneOptions{ +// Selection: []excelize.Selection{ // {SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"}, // }, // }) @@ -889,7 +889,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error { // YSplit: 1800, // TopLeftCell: "N57", // ActivePane: "bottomLeft", -// Panes: []excelize.PaneOptions{ +// Selection: []excelize.Selection{ // {SQRef: "I36", ActiveCell: "I36"}, // {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"}, // {SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"}, @@ -908,6 +908,50 @@ func (f *File) SetPanes(sheet string, panes *Panes) error { return ws.setPanes(panes) } +// getPanes returns freeze panes, split panes, and views of the worksheet. +func (ws *xlsxWorksheet) getPanes() Panes { + var ( + panes Panes + section []Selection + ) + if ws.SheetViews == nil || len(ws.SheetViews.SheetView) < 1 { + return panes + } + sw := ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1] + for _, s := range sw.Selection { + if s != nil { + section = append(section, Selection{ + SQRef: s.SQRef, + ActiveCell: s.ActiveCell, + Pane: s.Pane, + }) + } + } + panes.Selection = section + if sw.Pane == nil { + return panes + } + panes.ActivePane = sw.Pane.ActivePane + if sw.Pane.State == "frozen" { + panes.Freeze = true + } + panes.TopLeftCell = sw.Pane.TopLeftCell + panes.XSplit = int(sw.Pane.XSplit) + panes.YSplit = int(sw.Pane.YSplit) + return panes +} + +// GetPanes provides a function to get freeze panes, split panes, and worksheet +// views by given worksheet name. +func (f *File) GetPanes(sheet string) (Panes, error) { + var panes Panes + ws, err := f.workSheetReader(sheet) + if err != nil { + return panes, err + } + return ws.getPanes(), err +} + // GetSheetVisible provides a function to get worksheet visible by given worksheet // name. For example, get visible state of Sheet1: // diff --git a/sheet_test.go b/sheet_test.go index cfed8fdb90..8d42fd54db 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -37,25 +37,29 @@ func TestNewSheet(t *testing.T) { assert.Equal(t, -1, sheetID) } -func TestSetPanes(t *testing.T) { +func TestPanes(t *testing.T) { f := NewFile() assert.NoError(t, f.SetPanes("Sheet1", &Panes{Freeze: false, Split: false})) _, err := f.NewSheet("Panes 2") assert.NoError(t, err) - assert.NoError(t, f.SetPanes("Panes 2", - &Panes{ - Freeze: true, - Split: false, - XSplit: 1, - YSplit: 0, - TopLeftCell: "B1", - ActivePane: "topRight", - Panes: []PaneOptions{ - {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, - }, + + expected := Panes{ + Freeze: true, + Split: false, + XSplit: 1, + YSplit: 0, + TopLeftCell: "B1", + ActivePane: "topRight", + Selection: []Selection{ + {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, }, - )) + } + assert.NoError(t, f.SetPanes("Panes 2", &expected)) + panes, err := f.GetPanes("Panes 2") + assert.NoError(t, err) + assert.Equal(t, expected, panes) + _, err = f.NewSheet("Panes 3") assert.NoError(t, err) assert.NoError(t, f.SetPanes("Panes 3", @@ -66,7 +70,7 @@ func TestSetPanes(t *testing.T) { YSplit: 1800, TopLeftCell: "N57", ActivePane: "bottomLeft", - Panes: []PaneOptions{ + Selection: []Selection{ {SQRef: "I36", ActiveCell: "I36"}, {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"}, {SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"}, @@ -84,7 +88,7 @@ func TestSetPanes(t *testing.T) { YSplit: 9, TopLeftCell: "A34", ActivePane: "bottomLeft", - Panes: []PaneOptions{ + Selection: []Selection{ {SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"}, }, }, @@ -94,6 +98,26 @@ func TestSetPanes(t *testing.T) { // Test set panes with invalid sheet name assert.EqualError(t, f.SetPanes("Sheet:1", &Panes{Freeze: false, Split: false}), ErrSheetNameInvalid.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) + + // Test get panes with empty sheet views + f = NewFile() + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{} + _, err = f.GetPanes("Sheet1") + assert.NoError(t, err) + // Test get panes without panes + ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{{}}} + _, err = f.GetPanes("Sheet1") + assert.NoError(t, err) + // Test get panes without sheet views + ws.(*xlsxWorksheet).SheetViews = nil + _, err = f.GetPanes("Sheet1") + assert.NoError(t, err) + // Test get panes on not exists worksheet + _, err = f.GetPanes("SheetN") + assert.EqualError(t, err, "sheet SheetN does not exist") + // Test add pane on empty sheet views worksheet f = NewFile() f.checked = nil @@ -107,7 +131,7 @@ func TestSetPanes(t *testing.T) { YSplit: 0, TopLeftCell: "B1", ActivePane: "topRight", - Panes: []PaneOptions{ + Selection: []Selection{ {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, }, }, diff --git a/sheetview.go b/sheetview.go index 65b1354c78..3ca3d8c482 100644 --- a/sheetview.go +++ b/sheetview.go @@ -61,11 +61,7 @@ func (view *xlsxSheetView) setSheetView(opts *ViewOptions) { view.TopLeftCell = *opts.TopLeftCell } if opts.View != nil { - if _, ok := map[string]interface{}{ - "normal": nil, - "pageLayout": nil, - "pageBreakPreview": nil, - }[*opts.View]; ok { + if inStrSlice([]string{"normal", "pageLayout", "pageBreakPreview"}, *opts.View, true) != -1 { view.View = *opts.View } } diff --git a/stream_test.go b/stream_test.go index d5f3ed21fd..406de65939 100644 --- a/stream_test.go +++ b/stream_test.go @@ -173,7 +173,7 @@ func TestStreamSetPanes(t *testing.T) { YSplit: 0, TopLeftCell: "B1", ActivePane: "topRight", - Panes: []PaneOptions{ + Selection: []Selection{ {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, }, } diff --git a/table.go b/table.go index b63fe276a4..d59656daec 100644 --- a/table.go +++ b/table.go @@ -430,22 +430,23 @@ func (f *File) writeAutoFilter(fc *xlsxFilterColumn, exp []int, tokens []string) var filters []*xlsxFilter filters = append(filters, &xlsxFilter{Val: tokens[0]}) fc.Filters = &xlsxFilters{Filter: filters} - } else if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 { + return + } + if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 { // Double equality with "or" operator. var filters []*xlsxFilter for _, v := range tokens { filters = append(filters, &xlsxFilter{Val: v}) } fc.Filters = &xlsxFilters{Filter: filters} - } else { - // Non default custom filter. - expRel := map[int]int{0: 0, 1: 2} - andRel := map[int]bool{0: true, 1: false} - for k, v := range tokens { - f.writeCustomFilter(fc, exp[expRel[k]], v) - if k == 1 { - fc.CustomFilters.And = andRel[exp[k]] - } + return + } + // Non default custom filter. + expRel, andRel := map[int]int{0: 0, 1: 2}, map[int]bool{0: true, 1: false} + for k, v := range tokens { + f.writeCustomFilter(fc, exp[expRel[k]], v) + if k == 1 { + fc.CustomFilters.And = andRel[exp[k]] } } } @@ -467,11 +468,11 @@ func (f *File) writeCustomFilter(fc *xlsxFilterColumn, operator int, val string) } if fc.CustomFilters != nil { fc.CustomFilters.CustomFilter = append(fc.CustomFilters.CustomFilter, &customFilter) - } else { - var customFilters []*xlsxCustomFilter - customFilters = append(customFilters, &customFilter) - fc.CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters} + return } + var customFilters []*xlsxCustomFilter + customFilters = append(customFilters, &customFilter) + fc.CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters} } // parseFilterExpression provides a function to converts the tokens of a @@ -488,8 +489,7 @@ func (f *File) parseFilterExpression(expression string, tokens []string) ([]int, if len(tokens) == 7 { // The number of tokens will be either 3 (for 1 expression) or 7 (for 2 // expressions). - conditional := 0 - c := tokens[3] + conditional, c := 0, tokens[3] if conditionFormat.MatchString(c) { conditional = 1 } @@ -501,17 +501,13 @@ func (f *File) parseFilterExpression(expression string, tokens []string) ([]int, if err != nil { return expressions, t, err } - expressions = []int{expression1[0], conditional, expression2[0]} - t = []string{token1, token2} - } else { - exp, token, err := f.parseFilterTokens(expression, tokens) - if err != nil { - return expressions, t, err - } - expressions = exp - t = []string{token} + return []int{expression1[0], conditional, expression2[0]}, []string{token1, token2}, nil + } + exp, token, err := f.parseFilterTokens(expression, tokens) + if err != nil { + return expressions, t, err } - return expressions, t, nil + return exp, []string{token}, nil } // parseFilterTokens provides a function to parse the 3 tokens of a filter @@ -534,7 +530,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str operator, ok := operators[strings.ToLower(tokens[1])] if !ok { // Convert the operator from a number to a descriptive string. - return []int{}, "", fmt.Errorf("unknown operator: %s", tokens[1]) + return []int{}, "", newUnknownFilterTokenError(tokens[1]) } token := tokens[2] // Special handling for Blanks/NonBlanks. @@ -563,8 +559,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str } // If the string token contains an Excel match character then change the // operator type to indicate a non "simple" equality. - re = matchFormat.MatchString(token) - if operator == 2 && re { + if re = matchFormat.MatchString(token); operator == 2 && re { operator = 22 } return []int{operator}, token, nil diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 79170deca5..22ec03e3b1 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -894,8 +894,8 @@ type SparklineOptions struct { EmptyCells string } -// PaneOptions directly maps the settings of the pane. -type PaneOptions struct { +// Selection directly maps the settings of the worksheet selection. +type Selection struct { SQRef string ActiveCell string Pane string @@ -909,7 +909,7 @@ type Panes struct { YSplit int TopLeftCell string ActivePane string - Panes []PaneOptions + Selection []Selection } // ConditionalFormatOptions directly maps the conditional format settings of the cells. From 8e891b52c65de3445abdd094204904e0c1b32ea3 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 12 Jun 2023 00:09:40 +0800 Subject: [PATCH 205/213] This closes #1560, fix incorrect row number when get object position --- col.go | 8 ++++---- picture_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/col.go b/col.go index 4d19a2aa3e..bbb5d29845 100644 --- a/col.go +++ b/col.go @@ -605,14 +605,14 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) // height # Height of object frame. func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int) { // Adjust start column for offsets that are greater than the col width. - for x1 >= f.getColWidth(sheet, col) { - x1 -= f.getColWidth(sheet, col) + for x1 >= f.getColWidth(sheet, col+1) { + x1 -= f.getColWidth(sheet, col+1) col++ } // Adjust start row for offsets that are greater than the row height. - for y1 >= f.getRowHeight(sheet, row) { - y1 -= f.getRowHeight(sheet, row) + for y1 >= f.getRowHeight(sheet, row+1) { + y1 -= f.getRowHeight(sheet, row+1) row++ } diff --git a/picture_test.go b/picture_test.go index 95bb39e9fa..54f9bb0cbc 100644 --- a/picture_test.go +++ b/picture_test.go @@ -108,7 +108,7 @@ func TestAddPictureErrors(t *testing.T) { assert.NoError(t, f.AddPicture("Sheet1", "Q7", filepath.Join("test", "images", "excel.wmf"), nil)) assert.NoError(t, f.AddPicture("Sheet1", "Q13", filepath.Join("test", "images", "excel.emz"), nil)) assert.NoError(t, f.AddPicture("Sheet1", "Q19", filepath.Join("test", "images", "excel.wmz"), nil)) - assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", &GraphicOptions{ScaleX: 2.1})) + assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", &GraphicOptions{ScaleX: 2.8})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx"))) assert.NoError(t, f.Close()) } From 9bc3fd7e9fe034be5ffc211a6aba1509d0afa032 Mon Sep 17 00:00:00 2001 From: chengxinyao <18811788263cxy@gmail.com> Date: Wed, 14 Jun 2023 22:49:40 +0800 Subject: [PATCH 206/213] This optimize the code, simplify unit test and drawing object position calculation (#1561) Co-authored-by: xinyao.cheng --- col.go | 19 +++++++------- drawing.go | 5 +--- merge_test.go | 67 +++++++++++++++++++++++++------------------------ picture.go | 5 +--- picture_test.go | 20 ++++++--------- shape.go | 5 +--- stream_test.go | 64 +++++++++++++++++++++++----------------------- 7 files changed, 84 insertions(+), 101 deletions(-) diff --git a/col.go b/col.go index bbb5d29845..13bf13978f 100644 --- a/col.go +++ b/col.go @@ -604,20 +604,21 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) // width # Width of object frame. // height # Height of object frame. func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int) { + colIdx, rowIdx := col-1, row-1 // Adjust start column for offsets that are greater than the col width. - for x1 >= f.getColWidth(sheet, col+1) { - x1 -= f.getColWidth(sheet, col+1) - col++ + for x1 >= f.getColWidth(sheet, colIdx+1) { + colIdx++ + x1 -= f.getColWidth(sheet, colIdx) } // Adjust start row for offsets that are greater than the row height. - for y1 >= f.getRowHeight(sheet, row+1) { - y1 -= f.getRowHeight(sheet, row+1) - row++ + for y1 >= f.getRowHeight(sheet, rowIdx+1) { + rowIdx++ + y1 -= f.getRowHeight(sheet, rowIdx) } // Initialized end cell to the same as the start cell. - colEnd, rowEnd := col, row + colEnd, rowEnd := colIdx, rowIdx width += x1 height += y1 @@ -635,9 +636,7 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh } // The end vertices are whatever is left from the width and height. - x2 := width - y2 := height - return col, row, colEnd, rowEnd, x2, y2 + return colIdx, rowIdx, colEnd, rowEnd, width, height } // getColWidth provides a function to get column width in pixels by given diff --git a/drawing.go b/drawing.go index 41302e429d..40559febe2 100644 --- a/drawing.go +++ b/drawing.go @@ -1297,12 +1297,9 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI if err != nil { return err } - colIdx := col - 1 - rowIdx := row - 1 - width = int(float64(width) * opts.ScaleX) height = int(float64(height) * opts.ScaleY) - colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.OffsetX, opts.OffsetY, width, height) + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height) content, cNvPrID, err := f.drawingParser(drawingXML) if err != nil { return err diff --git a/merge_test.go b/merge_test.go index 2f15a3d5bb..18fa0f9372 100644 --- a/merge_test.go +++ b/merge_test.go @@ -13,14 +13,18 @@ func TestMergeCell(t *testing.T) { t.FailNow() } assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.NoError(t, f.MergeCell("Sheet1", "D9", "D9")) - assert.NoError(t, f.MergeCell("Sheet1", "D9", "E9")) - assert.NoError(t, f.MergeCell("Sheet1", "H14", "G13")) - assert.NoError(t, f.MergeCell("Sheet1", "C9", "D8")) - assert.NoError(t, f.MergeCell("Sheet1", "F11", "G13")) - assert.NoError(t, f.MergeCell("Sheet1", "H7", "B15")) - assert.NoError(t, f.MergeCell("Sheet1", "D11", "F13")) - assert.NoError(t, f.MergeCell("Sheet1", "G10", "K12")) + for _, cells := range [][]string{ + {"D9", "D9"}, + {"D9", "E9"}, + {"H14", "G13"}, + {"C9", "D8"}, + {"F11", "G13"}, + {"H7", "B15"}, + {"D11", "F13"}, + {"G10", "K12"}, + } { + assert.NoError(t, f.MergeCell("Sheet1", cells[0], cells[1])) + } assert.NoError(t, f.SetCellValue("Sheet1", "G11", "set value in merged cell")) assert.NoError(t, f.SetCellInt("Sheet1", "H11", 100)) assert.NoError(t, f.SetCellValue("Sheet1", "I11", 0.5)) @@ -39,32 +43,29 @@ func TestMergeCell(t *testing.T) { _, err = f.NewSheet("Sheet3") assert.NoError(t, err) - assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13")) - assert.NoError(t, f.MergeCell("Sheet3", "G10", "K12")) - - assert.NoError(t, f.MergeCell("Sheet3", "B1", "D5")) // B1:D5 - assert.NoError(t, f.MergeCell("Sheet3", "E1", "F5")) // E1:F5 - - assert.NoError(t, f.MergeCell("Sheet3", "H2", "I5")) - assert.NoError(t, f.MergeCell("Sheet3", "I4", "J6")) // H2:J6 - - assert.NoError(t, f.MergeCell("Sheet3", "M2", "N5")) - assert.NoError(t, f.MergeCell("Sheet3", "L4", "M6")) // L2:N6 - - assert.NoError(t, f.MergeCell("Sheet3", "P4", "Q7")) - assert.NoError(t, f.MergeCell("Sheet3", "O2", "P5")) // O2:Q7 - assert.NoError(t, f.MergeCell("Sheet3", "A9", "B12")) - assert.NoError(t, f.MergeCell("Sheet3", "B7", "C9")) // A7:C12 - - assert.NoError(t, f.MergeCell("Sheet3", "E9", "F10")) - assert.NoError(t, f.MergeCell("Sheet3", "D8", "G12")) - - assert.NoError(t, f.MergeCell("Sheet3", "I8", "I12")) - assert.NoError(t, f.MergeCell("Sheet3", "I10", "K10")) - - assert.NoError(t, f.MergeCell("Sheet3", "M8", "Q13")) - assert.NoError(t, f.MergeCell("Sheet3", "N10", "O11")) + for _, cells := range [][]string{ + {"D11", "F13"}, + {"G10", "K12"}, + {"B1", "D5"}, // B1:D5 + {"E1", "F5"}, // E1:F5 + {"H2", "I5"}, + {"I4", "J6"}, // H2:J6 + {"M2", "N5"}, + {"L4", "M6"}, // L2:N6 + {"P4", "Q7"}, + {"O2", "P5"}, // O2:Q7 + {"A9", "B12"}, + {"B7", "C9"}, // A7:C12 + {"E9", "F10"}, + {"D8", "G12"}, + {"I8", "I12"}, + {"I10", "K10"}, + {"M8", "Q13"}, + {"N10", "O11"}, + } { + assert.NoError(t, f.MergeCell("Sheet3", cells[0], cells[1])) + } // Test merge cells on not exists worksheet assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN does not exist") diff --git a/picture.go b/picture.go index c30d307b04..fb14c951b2 100644 --- a/picture.go +++ b/picture.go @@ -332,16 +332,13 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper } width, height := img.Width, img.Height if opts.AutoFit { - width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts) - if err != nil { + if width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts); err != nil { return err } } else { width = int(float64(width) * opts.ScaleX) height = int(float64(height) * opts.ScaleY) } - col-- - row-- colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height) content, cNvPrID, err := f.drawingParser(drawingXML) if err != nil { diff --git a/picture_test.go b/picture_test.go index 54f9bb0cbc..a2e0fb726c 100644 --- a/picture_test.go +++ b/picture_test.go @@ -62,10 +62,9 @@ func TestAddPicture(t *testing.T) { // Test add picture to worksheet from bytes with illegal cell reference assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.NoError(t, f.AddPicture("Sheet1", "Q8", filepath.Join("test", "images", "excel.gif"), nil)) - assert.NoError(t, f.AddPicture("Sheet1", "Q15", filepath.Join("test", "images", "excel.jpg"), nil)) - assert.NoError(t, f.AddPicture("Sheet1", "Q22", filepath.Join("test", "images", "excel.tif"), nil)) - assert.NoError(t, f.AddPicture("Sheet1", "Q28", filepath.Join("test", "images", "excel.bmp"), nil)) + for cell, ext := range map[string]string{"Q8": "gif", "Q15": "jpg", "Q22": "tif", "Q28": "bmp"} { + assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", fmt.Sprintf("excel.%s", ext)), nil)) + } // Test write file to given path assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx"))) @@ -99,15 +98,10 @@ func TestAddPictureErrors(t *testing.T) { // Test add picture with custom image decoder and encoder decode := func(r io.Reader) (image.Image, error) { return nil, nil } decodeConfig := func(r io.Reader) (image.Config, error) { return image.Config{Height: 100, Width: 90}, nil } - image.RegisterFormat("emf", "", decode, decodeConfig) - image.RegisterFormat("wmf", "", decode, decodeConfig) - image.RegisterFormat("emz", "", decode, decodeConfig) - image.RegisterFormat("wmz", "", decode, decodeConfig) - image.RegisterFormat("svg", "", decode, decodeConfig) - assert.NoError(t, f.AddPicture("Sheet1", "Q1", filepath.Join("test", "images", "excel.emf"), nil)) - assert.NoError(t, f.AddPicture("Sheet1", "Q7", filepath.Join("test", "images", "excel.wmf"), nil)) - assert.NoError(t, f.AddPicture("Sheet1", "Q13", filepath.Join("test", "images", "excel.emz"), nil)) - assert.NoError(t, f.AddPicture("Sheet1", "Q19", filepath.Join("test", "images", "excel.wmz"), nil)) + for cell, ext := range map[string]string{"Q1": "emf", "Q7": "wmf", "Q13": "emz", "Q19": "wmz"} { + image.RegisterFormat(ext, "", decode, decodeConfig) + assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", fmt.Sprintf("excel.%s", ext)), nil)) + } assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", &GraphicOptions{ScaleX: 2.8})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx"))) assert.NoError(t, f.Close()) diff --git a/shape.go b/shape.go index cb8f49d5b4..8d9f81455d 100644 --- a/shape.go +++ b/shape.go @@ -326,13 +326,10 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro if err != nil { return err } - colIdx := fromCol - 1 - rowIdx := fromRow - 1 - width := int(float64(opts.Width) * opts.Format.ScaleX) height := int(float64(opts.Height) * opts.Format.ScaleY) - colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.Format.OffsetX, opts.Format.OffsetY, + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, fromCol, fromRow, opts.Format.OffsetX, opts.Format.OffsetY, width, height) content, cNvPrID, err := f.drawingParser(drawingXML) if err != nil { diff --git a/stream_test.go b/stream_test.go index 406de65939..2f68d62ed7 100644 --- a/stream_test.go +++ b/stream_test.go @@ -337,11 +337,9 @@ func TestStreamSetRowWithStyle(t *testing.T) { ws, err := file.workSheetReader("Sheet1") assert.NoError(t, err) - assert.Equal(t, grayStyleID, ws.SheetData.Row[0].C[0].S) - assert.Equal(t, zeroStyleID, ws.SheetData.Row[0].C[1].S) - assert.Equal(t, zeroStyleID, ws.SheetData.Row[0].C[2].S) - assert.Equal(t, blueStyleID, ws.SheetData.Row[0].C[3].S) - assert.Equal(t, blueStyleID, ws.SheetData.Row[0].C[4].S) + for colIdx, expected := range []int{grayStyleID, zeroStyleID, zeroStyleID, blueStyleID, blueStyleID} { + assert.Equal(t, expected, ws.SheetData.Row[0].C[colIdx].S) + } } func TestStreamSetCellValFunc(t *testing.T) { @@ -352,25 +350,29 @@ func TestStreamSetCellValFunc(t *testing.T) { sw, err := f.NewStreamWriter("Sheet1") assert.NoError(t, err) c := &xlsxC{} - assert.NoError(t, sw.setCellValFunc(c, 128)) - assert.NoError(t, sw.setCellValFunc(c, int8(-128))) - assert.NoError(t, sw.setCellValFunc(c, int16(-32768))) - assert.NoError(t, sw.setCellValFunc(c, int32(-2147483648))) - assert.NoError(t, sw.setCellValFunc(c, int64(-9223372036854775808))) - assert.NoError(t, sw.setCellValFunc(c, uint(128))) - assert.NoError(t, sw.setCellValFunc(c, uint8(255))) - assert.NoError(t, sw.setCellValFunc(c, uint16(65535))) - assert.NoError(t, sw.setCellValFunc(c, uint32(4294967295))) - assert.NoError(t, sw.setCellValFunc(c, uint64(18446744073709551615))) - assert.NoError(t, sw.setCellValFunc(c, float32(100.1588))) - assert.NoError(t, sw.setCellValFunc(c, 100.1588)) - assert.NoError(t, sw.setCellValFunc(c, " Hello")) - assert.NoError(t, sw.setCellValFunc(c, []byte(" Hello"))) - assert.NoError(t, sw.setCellValFunc(c, time.Now().UTC())) - assert.NoError(t, sw.setCellValFunc(c, time.Duration(1e13))) - assert.NoError(t, sw.setCellValFunc(c, true)) - assert.NoError(t, sw.setCellValFunc(c, nil)) - assert.NoError(t, sw.setCellValFunc(c, complex64(5+10i))) + for _, val := range []interface{}{ + 128, + int8(-128), + int16(-32768), + int32(-2147483648), + int64(-9223372036854775808), + uint(128), + uint8(255), + uint16(65535), + uint32(4294967295), + uint64(18446744073709551615), + float32(100.1588), + 100.1588, + " Hello", + []byte(" Hello"), + time.Now().UTC(), + time.Duration(1e13), + true, + nil, + complex64(5 + 10i), + } { + assert.NoError(t, sw.setCellValFunc(c, val)) + } } func TestStreamWriterOutlineLevel(t *testing.T) { @@ -389,14 +391,10 @@ func TestStreamWriterOutlineLevel(t *testing.T) { file, err = OpenFile(filepath.Join("test", "TestStreamWriterSetRowOutlineLevel.xlsx")) assert.NoError(t, err) - level, err := file.GetRowOutlineLevel("Sheet1", 1) - assert.NoError(t, err) - assert.Equal(t, uint8(1), level) - level, err = file.GetRowOutlineLevel("Sheet1", 2) - assert.NoError(t, err) - assert.Equal(t, uint8(7), level) - level, err = file.GetRowOutlineLevel("Sheet1", 3) - assert.NoError(t, err) - assert.Equal(t, uint8(0), level) + for rowIdx, expected := range []uint8{1, 7, 0} { + level, err := file.GetRowOutlineLevel("Sheet1", rowIdx+1) + assert.NoError(t, err) + assert.Equal(t, expected, level) + } assert.NoError(t, file.Close()) } From f8aa3adf7e6dd419929feb5059f89cb97a8631cf Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 18 Jun 2023 00:12:50 +0800 Subject: [PATCH 207/213] This closes #1553, the `AddChart` function support set primary titles - Update unit tests and documentation - Lint issues fixed --- chart.go | 18 ++++++++-- chart_test.go | 2 +- drawing.go | 93 ++++++++++++++++++++++++++++++++++---------------- numfmt.go | 16 ++++----- numfmt_test.go | 2 +- xmlChart.go | 4 ++- 6 files changed, 92 insertions(+), 43 deletions(-) diff --git a/chart.go b/chart.go index b4a73feb5a..65200e80e2 100644 --- a/chart.go +++ b/chart.go @@ -794,6 +794,8 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Maximum // Minimum // Font +// NumFmt +// Title // // The properties of 'YAxis' that can be set are: // @@ -805,6 +807,9 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Maximum // Minimum // Font +// LogBase +// NumFmt +// Title // // None: Disable axes. // @@ -813,14 +818,14 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // MinorGridLines: Specifies minor grid lines. // // MajorUnit: Specifies the distance between major ticks. Shall contain a -// positive floating-point number. The MajorUnit property is optional. The +// positive floating-point number. The 'MajorUnit' property is optional. The // default value is auto. // // TickLabelSkip: Specifies how many tick labels to skip between label that is // drawn. The 'TickLabelSkip' property is optional. The default value is auto. // // ReverseOrder: Specifies that the categories or values on reverse order -// (orientation of the chart). The ReverseOrder property is optional. The +// (orientation of the chart). The 'ReverseOrder' property is optional. The // default value is false. // // Maximum: Specifies that the fixed maximum, 0 is auto. The 'Maximum' property @@ -841,6 +846,15 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Color // VertAlign // +// LogBase: Specifies logarithmic scale for the YAxis. +// +// NumFmt: Specifies that if linked to source and set custom number format code +// for axis. The 'NumFmt' property is optional. The default format code is +// 'General'. +// +// Title: Specifies that the primary horizontal or vertical axis title. The +// 'Title' property is optional. +// // Set chart size by 'Dimension' property. The 'Dimension' property is optional. // The default width is 480, and height is 290. // diff --git a/chart_test.go b/chart_test.go index ba17dbd611..4c359d7b51 100644 --- a/chart_test.go +++ b/chart_test.go @@ -206,7 +206,7 @@ func TestAddChart(t *testing.T) { sheetName, cell string opts *Chart }{ - {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "000000"}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}}}}, + {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "000000"}, Title: []RichTextRun{{Text: "Primary Horizontal Axis Title"}}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}, Title: []RichTextRun{{Text: "Primary Vertical Axis Title", Font: &Font{Color: "777777", Bold: true, Italic: true, Size: 12}}}}}}, {sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: ColStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Clustered Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, diff --git a/drawing.go b/drawing.go index 40559febe2..c7264177ed 100644 --- a/drawing.go +++ b/drawing.go @@ -66,41 +66,43 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { Title: &cTitle{ Tx: cTx{ Rich: &cRich{ - P: aP{ - PPr: &aPPr{ - DefRPr: aRPr{ - Kern: 1200, - Strike: "noStrike", - U: "none", - Sz: 1400, - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "tx1", - LumMod: &attrValInt{ - Val: intPtr(65000), - }, - LumOff: &attrValInt{ - Val: intPtr(35000), + P: []aP{ + { + PPr: &aPPr{ + DefRPr: aRPr{ + Kern: 1200, + Strike: "noStrike", + U: "none", + Sz: 1400, + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "tx1", + LumMod: &attrValInt{ + Val: intPtr(65000), + }, + LumOff: &attrValInt{ + Val: intPtr(35000), + }, }, }, - }, - Ea: &aEa{ - Typeface: "+mn-ea", - }, - Cs: &aCs{ - Typeface: "+mn-cs", - }, - Latin: &xlsxCTTextFont{ - Typeface: "+mn-lt", + Ea: &aEa{ + Typeface: "+mn-ea", + }, + Cs: &aCs{ + Typeface: "+mn-cs", + }, + Latin: &xlsxCTTextFont{ + Typeface: "+mn-lt", + }, }, }, - }, - R: &aR{ - RPr: aRPr{ - Lang: "en-US", - AltLang: "en-US", + R: &aR{ + RPr: aRPr{ + Lang: "en-US", + AltLang: "en-US", + }, + T: opts.Title.Name, }, - T: opts.Title.Name, }, }, }, @@ -1059,6 +1061,7 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { NumFmt: &cNumFmt{FormatCode: "General"}, MajorTickMark: &attrValString{Val: stringPtr("none")}, MinorTickMark: &attrValString{Val: stringPtr("none")}, + Title: f.drawPlotAreaTitles(opts.XAxis.Title, ""), TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(&opts.YAxis), @@ -1110,6 +1113,7 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { }, Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)}, AxPos: &attrValString{Val: stringPtr(valAxPos[opts.YAxis.ReverseOrder])}, + Title: f.drawPlotAreaTitles(opts.YAxis.Title, "horz"), NumFmt: &cNumFmt{ FormatCode: chartValAxNumFmtFormatCode[opts.Type], }, @@ -1169,6 +1173,35 @@ func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs { } } +// drawPlotAreaTitles provides a function to draw the c:title element. +func (f *File) drawPlotAreaTitles(runs []RichTextRun, vert string) *cTitle { + if len(runs) == 0 { + return nil + } + title := &cTitle{Tx: cTx{Rich: &cRich{}}, Overlay: &attrValBool{Val: boolPtr(false)}} + for _, run := range runs { + r := &aR{T: run.Text} + if run.Font != nil { + r.RPr.B, r.RPr.I = run.Font.Bold, run.Font.Italic + if run.Font.Color != "" { + r.RPr.SolidFill = &aSolidFill{SrgbClr: &attrValString{Val: stringPtr(run.Font.Color)}} + } + if run.Font.Size > 0 { + r.RPr.Sz = run.Font.Size * 100 + } + } + title.Tx.Rich.P = append(title.Tx.Rich.P, aP{ + PPr: &aPPr{DefRPr: aRPr{}}, + R: r, + EndParaRPr: &aEndParaRPr{Lang: "en-US", AltLang: "en-US"}, + }) + } + if vert == "horz" { + title.Tx.Rich.BodyPr = aBodyPr{Rot: -5400000, Vert: vert} + } + return title +} + // drawPlotAreaSpPr provides a function to draw the c:spPr element. func (f *File) drawPlotAreaSpPr() *cSpPr { return &cSpPr{ diff --git a/numfmt.go b/numfmt.go index 012325443a..da387fb384 100644 --- a/numfmt.go +++ b/numfmt.go @@ -1190,7 +1190,7 @@ func (nf *numberFormat) printNumberLiteral(text string) string { } for _, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { - if err, changeNumFmtCode := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { + if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { return nf.value } result += nf.currencyString @@ -1326,7 +1326,7 @@ func (nf *numberFormat) dateTimeHandler() string { nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false for i, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { - if err, changeNumFmtCode := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { + if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { return nf.value } nf.result += nf.currencyString @@ -1397,28 +1397,28 @@ func (nf *numberFormat) positiveHandler() string { // currencyLanguageHandler will be handling currency and language types tokens // for a number format expression. -func (nf *numberFormat) currencyLanguageHandler(token nfp.Token) (error, bool) { +func (nf *numberFormat) currencyLanguageHandler(token nfp.Token) (bool, error) { for _, part := range token.Parts { if inStrSlice(supportedTokenTypes, part.Token.TType, true) == -1 { - return ErrUnsupportedNumberFormat, false + return false, ErrUnsupportedNumberFormat } if part.Token.TType == nfp.TokenSubTypeLanguageInfo { if strings.EqualFold(part.Token.TValue, "F800") { // [$-x-sysdate] if nf.opts != nil && nf.opts.LongDatePattern != "" { nf.value = format(nf.value, nf.opts.LongDatePattern, nf.date1904, nf.cellType, nf.opts) - return nil, true + return true, nil } part.Token.TValue = "409" } if strings.EqualFold(part.Token.TValue, "F400") { // [$-x-systime] if nf.opts != nil && nf.opts.LongTimePattern != "" { nf.value = format(nf.value, nf.opts.LongTimePattern, nf.date1904, nf.cellType, nf.opts) - return nil, true + return true, nil } part.Token.TValue = "409" } if _, ok := supportedLanguageInfo[strings.ToUpper(part.Token.TValue)]; !ok { - return ErrUnsupportedNumberFormat, false + return false, ErrUnsupportedNumberFormat } nf.localCode = strings.ToUpper(part.Token.TValue) } @@ -1426,7 +1426,7 @@ func (nf *numberFormat) currencyLanguageHandler(token nfp.Token) (error, bool) { nf.currencyString = part.Token.TValue } } - return nil, false + return false, nil } // localAmPm return AM/PM name by supported language ID. diff --git a/numfmt_test.go b/numfmt_test.go index c49393f661..8cf7afadd3 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -1093,7 +1093,7 @@ func TestNumFmt(t *testing.T) { } } nf := numberFormat{} - err, changeNumFmtCode := nf.currencyLanguageHandler(nfp.Token{Parts: []nfp.Part{{}}}) + changeNumFmtCode, err := nf.currencyLanguageHandler(nfp.Token{Parts: []nfp.Part{{}}}) assert.Equal(t, ErrUnsupportedNumberFormat, err) assert.False(t, changeNumFmtCode) } diff --git a/xmlChart.go b/xmlChart.go index 20b70517f9..8e9e46ce9b 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -74,7 +74,7 @@ type cTx struct { type cRich struct { BodyPr aBodyPr `xml:"a:bodyPr,omitempty"` LstStyle string `xml:"a:lstStyle,omitempty"` - P aP `xml:"a:p"` + P []aP `xml:"a:p"` } // aBodyPr (Body Properties) directly maps the a:bodyPr element. This element @@ -351,6 +351,7 @@ type cAxs struct { AxPos *attrValString `xml:"axPos"` MajorGridlines *cChartLines `xml:"majorGridlines"` MinorGridlines *cChartLines `xml:"minorGridlines"` + Title *cTitle `xml:"title"` NumFmt *cNumFmt `xml:"numFmt"` MajorTickMark *attrValString `xml:"majorTickMark"` MinorTickMark *attrValString `xml:"minorTickMark"` @@ -539,6 +540,7 @@ type ChartAxis struct { Font Font LogBase float64 NumFmt ChartNumFmt + Title []RichTextRun } // ChartDimension directly maps the dimension of the chart. From dcb26b2cb8bec11c803cae35fa95e6906b8fef37 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 30 Jun 2023 05:02:18 +0000 Subject: [PATCH 208/213] Made unit tests compatibility with the next Go language version - Fix documents issues for the `AddChart` function - Update GitHub sponsor profile --- .github/FUNDING.yml | 3 ++- chart.go | 8 ++++---- lib_test.go | 6 ++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ff137ebd4c..ab9fc53ec3 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,5 +1,6 @@ -patreon: xuri +github: xuri open_collective: excelize +patreon: xuri ko_fi: xurime liberapay: xuri issuehunt: xuri diff --git a/chart.go b/chart.go index 65200e80e2..0fea7712c3 100644 --- a/chart.go +++ b/chart.go @@ -846,17 +846,17 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Color // VertAlign // -// LogBase: Specifies logarithmic scale for the YAxis. +// LogBase: Specifies logarithmic scale base number of the vertical axis. // // NumFmt: Specifies that if linked to source and set custom number format code // for axis. The 'NumFmt' property is optional. The default format code is // 'General'. // -// Title: Specifies that the primary horizontal or vertical axis title. The -// 'Title' property is optional. +// Title: Specifies that the primary horizontal or vertical axis title and +// resize chart. The 'Title' property is optional. // // Set chart size by 'Dimension' property. The 'Dimension' property is optional. -// The default width is 480, and height is 290. +// The default width is 480, and height is 260. // // combo: Specifies the create a chart that combines two or more chart types in // a single chart. For example, create a clustered column - line chart with diff --git a/lib_test.go b/lib_test.go index 013cf05316..fe8d6a8ffb 100644 --- a/lib_test.go +++ b/lib_test.go @@ -342,10 +342,8 @@ func TestReadBytes(t *testing.T) { } func TestUnzipToTemp(t *testing.T) { - for _, v := range []string{"go1.19", "go1.20"} { - if strings.HasPrefix(runtime.Version(), v) { - t.Skip() - } + if ver := runtime.Version(); strings.HasPrefix(ver, "go1.19") || strings.HasPrefix(ver, "go1.2") { + t.Skip() } os.Setenv("TMPDIR", "test") defer os.Unsetenv("TMPDIR") From 700af6a5298010dfaf56abdbb6aa66fa85c2784d Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 3 Jul 2023 00:05:26 +0800 Subject: [PATCH 209/213] This fixed #1564, apply all of its arguments that meet multiple criteria --- calc.go | 6 ++---- calc_test.go | 16 +++++++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/calc.go b/calc.go index 8e37bb97d0..f3892fca38 100644 --- a/calc.go +++ b/calc.go @@ -7812,6 +7812,7 @@ func formulaIfsMatch(args []formulaArg) (cellRefs []cellRef) { } } } else { + match = []cellRef{} for _, ref := range cellRefs { value := matrix[ref.Row][ref.Col] if ok, _ := formulaCriteriaEval(value.Value(), criteria); ok { @@ -7819,9 +7820,6 @@ func formulaIfsMatch(args []formulaArg) (cellRefs []cellRef) { } } } - if len(match) == 0 { - return - } cellRefs = match[:] } return @@ -14397,7 +14395,7 @@ func (fn *formulaFuncs) MATCH(argsList *list.List) formulaArg { } switch lookupArrayArg.Type { case ArgMatrix: - if len(lookupArrayArg.Matrix[0]) != 1 { + if len(lookupArrayArg.Matrix) != 1 && len(lookupArrayArg.Matrix[0]) != 1 { return newErrorFormulaArg(formulaErrorNA, lookupArrayErr) } lookupArray = lookupArrayArg.ToList() diff --git a/calc_test.go b/calc_test.go index a706f3de8f..607289f2e4 100644 --- a/calc_test.go +++ b/calc_test.go @@ -3828,7 +3828,8 @@ func TestCalcCellValue(t *testing.T) { "=MATCH(0,A1:A1,0,0)": {"#VALUE!", "MATCH requires 1 or 2 arguments"}, "=MATCH(0,A1:A1,\"x\")": {"#VALUE!", "MATCH requires numeric match_type argument"}, "=MATCH(0,A1)": {"#N/A", "MATCH arguments lookup_array should be one-dimensional array"}, - "=MATCH(0,A1:B1)": {"#N/A", "MATCH arguments lookup_array should be one-dimensional array"}, + "=MATCH(0,A1:B2)": {"#N/A", "MATCH arguments lookup_array should be one-dimensional array"}, + "=MATCH(0,A1:B1)": {"#N/A", "#N/A"}, // TRANSPOSE "=TRANSPOSE()": {"#VALUE!", "TRANSPOSE requires 1 argument"}, // HYPERLINK @@ -5131,10 +5132,14 @@ func TestCalcSUMIFSAndAVERAGEIFS(t *testing.T) { } f := prepareCalcData(cellData) formulaList := map[string]string{ - "=AVERAGEIFS(D2:D13,A2:A13,1,B2:B13,\"North\")": "174000", - "=AVERAGEIFS(D2:D13,A2:A13,\">2\",C2:C13,\"Jeff\")": "285500", - "=SUMIFS(D2:D13,A2:A13,1,B2:B13,\"North\")": "348000", - "=SUMIFS(D2:D13,A2:A13,\">2\",C2:C13,\"Jeff\")": "571000", + "=AVERAGEIFS(D2:D13,A2:A13,1,B2:B13,\"North\")": "174000", + "=AVERAGEIFS(D2:D13,A2:A13,\">2\",C2:C13,\"Jeff\")": "285500", + "=SUMIFS(D2:D13,A2:A13,1,B2:B13,\"North\")": "348000", + "=SUMIFS(D2:D13,A2:A13,\">2\",C2:C13,\"Jeff\")": "571000", + "=SUMIFS(D2:D13,A2:A13,1,D2:D13,125000)": "125000", + "=SUMIFS(D2:D13,A2:A13,1,D2:D13,\">100000\",C2:C13,\"Chris\")": "125000", + "=SUMIFS(D2:D13,A2:A13,1,D2:D13,\"<40000\",C2:C13,\"Chris\")": "0", + "=SUMIFS(D2:D13,A2:A13,1,A2:A13,2)": "0", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula)) @@ -5147,6 +5152,7 @@ func TestCalcSUMIFSAndAVERAGEIFS(t *testing.T) { "=AVERAGEIFS(H1,\"\")": {"#VALUE!", "AVERAGEIFS requires at least 3 arguments"}, "=AVERAGEIFS(H1,\"\",TRUE,1)": {"#N/A", "#N/A"}, "=AVERAGEIFS(H1,\"\",TRUE)": {"#DIV/0!", "AVERAGEIF divide by zero"}, + "=AVERAGEIFS(D2:D13,A2:A13,1,A2:A13,2)": {"#DIV/0!", "AVERAGEIF divide by zero"}, "=SUMIFS()": {"#VALUE!", "SUMIFS requires at least 3 arguments"}, "=SUMIFS(D2:D13,A2:A13,1,B2:B13)": {"#N/A", "#N/A"}, "=SUMIFS(D20:D23,A2:A13,\">2\",C2:C13,\"Jeff\")": {"#VALUE!", "#VALUE!"}, From e2c74162925c7dc7e87c8818b0b83cf4dd3dbc40 Mon Sep 17 00:00:00 2001 From: lidp20 <1697871629@qq.com> Date: Tue, 4 Jul 2023 00:06:37 +0800 Subject: [PATCH 210/213] This closes #1565, support adjust formula when instering columns and rows (#1567) --- adjust.go | 30 ++++++++++++++++++++++++++++-- adjust_test.go | 20 ++++++++++++++++++++ rows.go | 2 +- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/adjust.go b/adjust.go index 7fc9faa48d..5f408979b6 100644 --- a/adjust.go +++ b/adjust.go @@ -131,6 +131,7 @@ func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) error { if cellCol, cellRow, _ := CellNameToCoordinates(v.R); col <= cellCol { if newCol := cellCol + offset; newCol > 0 { ws.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow) + _ = f.adjustFormula(ws.SheetData.Row[rowIdx].C[colIdx].F, columns, offset, false) } } } @@ -152,21 +153,46 @@ func (f *File) adjustRowDimensions(ws *xlsxWorksheet, row, offset int) error { for i := 0; i < len(ws.SheetData.Row); i++ { r := &ws.SheetData.Row[i] if newRow := r.R + offset; r.R >= row && newRow > 0 { - f.adjustSingleRowDimensions(r, newRow) + f.adjustSingleRowDimensions(r, newRow, offset, false) } } return nil } // adjustSingleRowDimensions provides a function to adjust single row dimensions. -func (f *File) adjustSingleRowDimensions(r *xlsxRow, num int) { +func (f *File) adjustSingleRowDimensions(r *xlsxRow, num, offset int, si bool) { r.R = num for i, col := range r.C { colName, _, _ := SplitCellName(col.R) r.C[i].R, _ = JoinCellName(colName, num) + _ = f.adjustFormula(col.F, rows, offset, si) } } +// adjustFormula provides a function to adjust shared formula reference. +func (f *File) adjustFormula(formula *xlsxF, dir adjustDirection, offset int, si bool) error { + if formula != nil && formula.Ref != "" { + coordinates, err := rangeRefToCoordinates(formula.Ref) + if err != nil { + return err + } + if dir == columns { + coordinates[0] += offset + coordinates[2] += offset + } else { + coordinates[1] += offset + coordinates[3] += offset + } + if formula.Ref, err = f.coordinatesToRangeRef(coordinates); err != nil { + return err + } + if si && formula.Si != nil { + formula.Si = intPtr(*formula.Si + 1) + } + } + return nil +} + // adjustHyperlinks provides a function to update hyperlinks when inserting or // deleting rows or columns. func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) { diff --git a/adjust_test.go b/adjust_test.go index c90a3f5cc8..f6147e6486 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -445,3 +445,23 @@ func TestAdjustCols(t *testing.T) { assert.NoError(t, f.Close()) } + +func TestAdjustFormula(t *testing.T) { + f := NewFile() + formulaType, ref := STCellFormulaTypeShared, "C1:C5" + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType})) + assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10)) + assert.NoError(t, f.InsertCols("Sheet1", "B", 1)) + assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) + for cell, expected := range map[string]string{"D2": "=A1+B1", "D3": "=A2+B2", "D11": "=A1+B1"} { + formula, err := f.GetCellFormula("Sheet1", cell) + assert.NoError(t, err) + assert.Equal(t, expected, formula) + } + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustFormula.xlsx"))) + assert.NoError(t, f.Close()) + + assert.NoError(t, f.adjustFormula(nil, rows, 0, false)) + assert.Equal(t, f.adjustFormula(&xlsxF{Ref: "-"}, rows, 0, false), ErrParameterInvalid) + assert.Equal(t, f.adjustFormula(&xlsxF{Ref: "XFD1:XFD1"}, columns, 1, false), ErrColumnNumber) +} diff --git a/rows.go b/rows.go index 7351d160c4..332bedda38 100644 --- a/rows.go +++ b/rows.go @@ -662,7 +662,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { } rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...) - f.adjustSingleRowDimensions(&rowCopy, row2) + f.adjustSingleRowDimensions(&rowCopy, row2, row2-row, true) if idx2 != -1 { ws.SheetData.Row[idx2] = rowCopy From fb72e56667c7d88d52757e39145ff9e190184523 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 6 Jul 2023 10:49:49 +0000 Subject: [PATCH 211/213] This closes #1569, formula function CONCAT, CONCATENATE support concatenation of multiple cell values --- calc.go | 21 +++++---------------- calc_test.go | 10 ++++++++-- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/calc.go b/calc.go index f3892fca38..3c449492c3 100644 --- a/calc.go +++ b/calc.go @@ -13282,24 +13282,13 @@ func (fn *formulaFuncs) CONCATENATE(argsList *list.List) formulaArg { // concat is an implementation of the formula functions CONCAT and // CONCATENATE. func (fn *formulaFuncs) concat(name string, argsList *list.List) formulaArg { - buf := bytes.Buffer{} + var buf bytes.Buffer for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(formulaArg) - switch token.Type { - case ArgString: - buf.WriteString(token.String) - case ArgNumber: - if token.Boolean { - if token.Number == 0 { - buf.WriteString("FALSE") - } else { - buf.WriteString("TRUE") - } - } else { - buf.WriteString(token.Value()) + for _, cell := range arg.Value.(formulaArg).ToList() { + if cell.Type == ArgError { + return cell } - default: - return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires arguments to be strings", name)) + buf.WriteString(cell.Value()) } } return newStringFormulaArg(buf.String()) diff --git a/calc_test.go b/calc_test.go index 607289f2e4..24c6efa3e8 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1663,8 +1663,12 @@ func TestCalcCellValue(t *testing.T) { "=CODE(\"\")": "0", // CONCAT "=CONCAT(TRUE(),1,FALSE(),\"0\",INT(2))": "TRUE1FALSE02", + "=CONCAT(MUNIT(2))": "1001", + "=CONCAT(A1:B2)": "1425", // CONCATENATE "=CONCATENATE(TRUE(),1,FALSE(),\"0\",INT(2))": "TRUE1FALSE02", + "=CONCATENATE(MUNIT(2))": "1001", + "=CONCATENATE(A1:B2)": "1425", // EXACT "=EXACT(1,\"1\")": "TRUE", "=EXACT(1,1)": "TRUE", @@ -3665,9 +3669,11 @@ func TestCalcCellValue(t *testing.T) { "=CODE()": {"#VALUE!", "CODE requires 1 argument"}, "=CODE(1,2)": {"#VALUE!", "CODE requires 1 argument"}, // CONCAT - "=CONCAT(MUNIT(2))": {"#VALUE!", "CONCAT requires arguments to be strings"}, + "=CONCAT(NA())": {"#N/A", "#N/A"}, + "=CONCAT(1,1/0)": {"#DIV/0!", "#DIV/0!"}, // CONCATENATE - "=CONCATENATE(MUNIT(2))": {"#VALUE!", "CONCATENATE requires arguments to be strings"}, + "=CONCATENATE(NA())": {"#N/A", "#N/A"}, + "=CONCATENATE(1,1/0)": {"#DIV/0!", "#DIV/0!"}, // EXACT "=EXACT()": {"#VALUE!", "EXACT requires 2 arguments"}, "=EXACT(1,2,3)": {"#VALUE!", "EXACT requires 2 arguments"}, From f5fe6d3fc930f49f7912f9b5ca09e5868917404d Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 7 Jul 2023 11:00:54 +0000 Subject: [PATCH 212/213] This closes #518, support creating chart with a secondary series axis --- chart.go | 5 +++ chart_test.go | 4 +- drawing.go | 112 ++++++++++++++++++++++++++++++++------------------ xmlChart.go | 2 + 4 files changed, 82 insertions(+), 41 deletions(-) diff --git a/chart.go b/chart.go index 0fea7712c3..83b653763c 100644 --- a/chart.go +++ b/chart.go @@ -803,6 +803,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // MajorGridLines // MinorGridLines // MajorUnit +// Secondary // ReverseOrder // Maximum // Minimum @@ -821,6 +822,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // positive floating-point number. The 'MajorUnit' property is optional. The // default value is auto. // +// Secondary: Specifies the current series vertical axis as the secondary axis, +// this only works for the second and later chart in the combo chart. The +// default value is false. +// // TickLabelSkip: Specifies how many tick labels to skip between label that is // drawn. The 'TickLabelSkip' property is optional. The default value is auto. // diff --git a/chart_test.go b/chart_test.go index 4c359d7b51..49b835514d 100644 --- a/chart_test.go +++ b/chart_test.go @@ -238,7 +238,7 @@ func TestAddChart(t *testing.T) { {sheetName: "Sheet2", cell: "P64", opts: &Chart{Type: BarPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked 100% Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "X64", opts: &Chart{Type: Bar3DClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "P80", opts: &Chart{Type: Bar3DStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{Maximum: &maximum, Minimum: &minimum}}}, - {sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: Bar3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}}, + {sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: Bar3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Secondary: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}}, // area series chart {sheetName: "Sheet2", cell: "AF1", opts: &Chart{Type: Area, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AN1", opts: &Chart{Type: AreaStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, @@ -280,7 +280,7 @@ func TestAddChart(t *testing.T) { {"I1", Doughnut, "Clustered Column - Doughnut Chart"}, } for _, props := range clusteredColumnCombo { - assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[2].(string)}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) + assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[2].(string)}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}, YAxis: ChartAxis{Secondary: true}})) } stackedAreaCombo := map[string][]interface{}{ "A16": {Line, "Stacked Area - Line Chart"}, diff --git a/drawing.go b/drawing.go index c7264177ed..9e2c9f3065 100644 --- a/drawing.go +++ b/drawing.go @@ -277,13 +277,10 @@ func (f *File) drawBaseChart(opts *Chart) *cPlotArea { VaryColors: &attrValBool{ Val: opts.VaryColors, }, - Ser: f.drawChartSeries(opts), - Shape: f.drawChartShape(opts), - DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + Ser: f.drawChartSeries(opts), + Shape: f.drawChartShape(opts), + DLbls: f.drawChartDLbls(opts), + AxID: f.genAxID(opts), Overlap: &attrValInt{Val: intPtr(100)}, } var ok bool @@ -542,10 +539,7 @@ func (f *File) drawLineChart(opts *Chart) *cPlotArea { }, Ser: f.drawChartSeries(opts), DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + AxID: f.genAxID(opts), }, CatAx: f.drawPlotAreaCatAx(opts), ValAx: f.drawPlotAreaValAx(opts), @@ -565,10 +559,7 @@ func (f *File) drawLine3DChart(opts *Chart) *cPlotArea { }, Ser: f.drawChartSeries(opts), DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + AxID: f.genAxID(opts), }, CatAx: f.drawPlotAreaCatAx(opts), ValAx: f.drawPlotAreaValAx(opts), @@ -658,10 +649,7 @@ func (f *File) drawRadarChart(opts *Chart) *cPlotArea { }, Ser: f.drawChartSeries(opts), DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + AxID: f.genAxID(opts), }, CatAx: f.drawPlotAreaCatAx(opts), ValAx: f.drawPlotAreaValAx(opts), @@ -681,10 +669,7 @@ func (f *File) drawScatterChart(opts *Chart) *cPlotArea { }, Ser: f.drawChartSeries(opts), DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + AxID: f.genAxID(opts), }, CatAx: f.drawPlotAreaCatAx(opts), ValAx: f.drawPlotAreaValAx(opts), @@ -698,9 +683,9 @@ func (f *File) drawSurface3DChart(opts *Chart) *cPlotArea { Surface3DChart: &cCharts{ Ser: f.drawChartSeries(opts), AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - {Val: intPtr(832256642)}, + {Val: intPtr(100000000)}, + {Val: intPtr(100000001)}, + {Val: intPtr(100000005)}, }, }, CatAx: f.drawPlotAreaCatAx(opts), @@ -720,9 +705,9 @@ func (f *File) drawSurfaceChart(opts *Chart) *cPlotArea { SurfaceChart: &cCharts{ Ser: f.drawChartSeries(opts), AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - {Val: intPtr(832256642)}, + {Val: intPtr(100000000)}, + {Val: intPtr(100000001)}, + {Val: intPtr(100000005)}, }, }, CatAx: f.drawPlotAreaCatAx(opts), @@ -745,10 +730,7 @@ func (f *File) drawBubbleChart(opts *Chart) *cPlotArea { }, Ser: f.drawChartSeries(opts), DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + AxID: f.genAxID(opts), }, ValAx: []*cAxs{f.drawPlotAreaCatAx(opts)[0], f.drawPlotAreaValAx(opts)[0]}, } @@ -1050,7 +1032,7 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { } axs := []*cAxs{ { - AxID: &attrValInt{Val: intPtr(754001152)}, + AxID: &attrValInt{Val: intPtr(100000000)}, Scaling: &cScaling{ Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])}, Max: max, @@ -1065,7 +1047,7 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(&opts.YAxis), - CrossAx: &attrValInt{Val: intPtr(753999904)}, + CrossAx: &attrValInt{Val: intPtr(100000001)}, Crosses: &attrValString{Val: stringPtr("autoZero")}, Auto: &attrValBool{Val: boolPtr(true)}, LblAlgn: &attrValString{Val: stringPtr("ctr")}, @@ -1085,6 +1067,28 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { if opts.XAxis.TickLabelSkip != 0 { axs[0].TickLblSkip = &attrValInt{Val: intPtr(opts.XAxis.TickLabelSkip)} } + if opts.order > 0 && opts.YAxis.Secondary { + axs = append(axs, &cAxs{ + AxID: &attrValInt{Val: intPtr(opts.XAxis.axID)}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: boolPtr(true)}, + AxPos: &attrValString{Val: stringPtr("b")}, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(&opts.YAxis), + CrossAx: &attrValInt{Val: intPtr(opts.YAxis.axID)}, + Auto: &attrValBool{Val: boolPtr(true)}, + LblAlgn: &attrValString{Val: stringPtr("ctr")}, + LblOffset: &attrValInt{Val: intPtr(100)}, + NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, + }) + } return axs } @@ -1104,7 +1108,7 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { } axs := []*cAxs{ { - AxID: &attrValInt{Val: intPtr(753999904)}, + AxID: &attrValInt{Val: intPtr(100000001)}, Scaling: &cScaling{ LogBase: logBase, Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, @@ -1122,7 +1126,7 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(&opts.XAxis), - CrossAx: &attrValInt{Val: intPtr(754001152)}, + CrossAx: &attrValInt{Val: intPtr(100000000)}, Crosses: &attrValString{Val: stringPtr("autoZero")}, CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, }, @@ -1142,6 +1146,26 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { if opts.YAxis.MajorUnit != 0 { axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(opts.YAxis.MajorUnit)} } + if opts.order > 0 && opts.YAxis.Secondary { + axs = append(axs, &cAxs{ + AxID: &attrValInt{Val: intPtr(opts.YAxis.axID)}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr("r")}, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(&opts.XAxis), + CrossAx: &attrValInt{Val: intPtr(opts.XAxis.axID)}, + Crosses: &attrValString{Val: stringPtr("max")}, + CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, + }) + } return axs } @@ -1157,7 +1181,7 @@ func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs { } return []*cAxs{ { - AxID: &attrValInt{Val: intPtr(832256642)}, + AxID: &attrValInt{Val: intPtr(100000005)}, Scaling: &cScaling{ Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, Max: max, @@ -1168,7 +1192,7 @@ func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs { TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(nil), - CrossAx: &attrValInt{Val: intPtr(753999904)}, + CrossAx: &attrValInt{Val: intPtr(100000001)}, }, } } @@ -1467,3 +1491,13 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) error f.Drawings.Store(drawingXML, wsDr) return err } + +// genAxID provides a function to generate ID for primary and secondary +// horizontal or vertical axis. +func (f *File) genAxID(opts *Chart) []*attrValInt { + opts.XAxis.axID, opts.YAxis.axID = 100000000, 100000001 + if opts.order > 0 && opts.YAxis.Secondary { + opts.XAxis.axID, opts.YAxis.axID = 100000003, 100000004 + } + return []*attrValInt{{Val: intPtr(opts.XAxis.axID)}, {Val: intPtr(opts.YAxis.axID)}} +} diff --git a/xmlChart.go b/xmlChart.go index 8e9e46ce9b..7be783bd68 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -535,12 +535,14 @@ type ChartAxis struct { MajorUnit float64 TickLabelSkip int ReverseOrder bool + Secondary bool Maximum *float64 Minimum *float64 Font Font LogBase float64 NumFmt ChartNumFmt Title []RichTextRun + axID int } // ChartDimension directly maps the dimension of the chart. From 8418bd7afd63fb822e5cee0c88aabf7d35029332 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 8 Jul 2023 18:36:35 +0800 Subject: [PATCH 213/213] This closes #1572 - Breaking changes: changed the data type for the `DecimalPlaces` to pointer of integer - Fallback to default 2 zero placeholder for invalid decimal places - Update unit tests --- excelize_test.go | 4 ++-- styles.go | 50 +++++++++++++++++++----------------------------- xmlStyles.go | 2 +- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 5ef9207be5..9372be5865 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -803,11 +803,11 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", "A1", 56)) assert.NoError(t, f.SetCellValue("Sheet1", "A2", -32.3)) var style int - style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: -1}) + style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: intPtr(-1)}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) - style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: 31, NegRed: true}) + style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: intPtr(31), NegRed: true}) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) diff --git a/styles.go b/styles.go index 70c11d596b..a2d021f267 100644 --- a/styles.go +++ b/styles.go @@ -977,8 +977,8 @@ func (f *File) NewStyle(style *Style) (int, error) { if err != nil { return cellXfsID, err } - if fs.DecimalPlaces == 0 { - fs.DecimalPlaces = 2 + if fs.DecimalPlaces != nil && (*fs.DecimalPlaces < 0 || *fs.DecimalPlaces > 30) { + fs.DecimalPlaces = intPtr(2) } f.mu.Lock() s, err := f.stylesReader() @@ -1037,7 +1037,7 @@ var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ if style.CustomNumFmt == nil && numFmtID == -1 { return xf.NumFmtID != nil && *xf.NumFmtID == 0 } - if style.NegRed || style.DecimalPlaces != 2 { + if style.NegRed || (style.DecimalPlaces != nil && *style.DecimalPlaces != 2) { return false } return xf.NumFmtID != nil && *xf.NumFmtID == numFmtID @@ -1291,13 +1291,12 @@ func getNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (numFmtID int) { // newNumFmt provides a function to check if number format code in the range // of built-in values. func newNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { - dp := "0." - numFmtID := 164 // Default custom number format code from 164. - if style.DecimalPlaces < 0 || style.DecimalPlaces > 30 { - style.DecimalPlaces = 2 - } - for i := 0; i < style.DecimalPlaces; i++ { - dp += "0" + dp, numFmtID := "0", 164 // Default custom number format code from 164. + if style.DecimalPlaces != nil && *style.DecimalPlaces > 0 { + dp += "." + for i := 0; i < *style.DecimalPlaces; i++ { + dp += "0" + } } if style.CustomNumFmt != nil { if customNumFmtID := getCustomNumFmtID(styleSheet, style); customNumFmtID != -1 { @@ -1305,35 +1304,26 @@ func newNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { } return setCustomNumFmt(styleSheet, style) } - _, ok := builtInNumFmt[style.NumFmt] - if !ok { + if _, ok := builtInNumFmt[style.NumFmt]; !ok { fc, currency := currencyNumFmt[style.NumFmt] if !currency { return setLangNumFmt(style) } - fc = strings.ReplaceAll(fc, "0.00", dp) + if style.DecimalPlaces != nil { + fc = strings.ReplaceAll(fc, "0.00", dp) + } if style.NegRed { fc = fc + ";[Red]" + fc } - if styleSheet.NumFmts != nil { - numFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1 - nf := xlsxNumFmt{ - FormatCode: fc, - NumFmtID: numFmtID, - } - styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf) - styleSheet.NumFmts.Count++ + if styleSheet.NumFmts == nil { + styleSheet.NumFmts = &xlsxNumFmts{NumFmt: []*xlsxNumFmt{}} } else { - nf := xlsxNumFmt{ - FormatCode: fc, - NumFmtID: numFmtID, - } - numFmts := xlsxNumFmts{ - NumFmt: []*xlsxNumFmt{&nf}, - Count: 1, - } - styleSheet.NumFmts = &numFmts + numFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1 } + styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &xlsxNumFmt{ + FormatCode: fc, NumFmtID: numFmtID, + }) + styleSheet.NumFmts.Count++ return numFmtID } return style.NumFmt diff --git a/xmlStyles.go b/xmlStyles.go index 74b9119b16..3a56f6f618 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -369,7 +369,7 @@ type Style struct { Alignment *Alignment Protection *Protection NumFmt int - DecimalPlaces int + DecimalPlaces *int CustomNumFmt *string NegRed bool }